求知若饥,虚心若愚。
本篇文章概要:
从类组件转变成hooks组件。这期间对于react研发来说,是个挑战,而且转变挺大的!
在讲hooks之前,说下类组件有哪些不足的地方?
1.类组件状态逻辑复用难
2.类组件趋向复杂难以维护
3.this指向困扰
hoos组件优势:
1.自定义hook方便复用状态逻辑
2.副作用的关注点分离
3.无this指向问题
后面会详细说明~
在没引入 hooks 之前,函数组件是没有state ,因此函数组件是不存在生命周期这一概念的;
但是引入 hooks 之后,函数组件支持了 state,所以就有了和类组件一样的生命周期这一概念。
下面来对比,函数组件hooks和类组件class生命周期的对应关系,这样习惯写类组件的react开发能很好的写出hooks组件。
先从react常用的五个生命周期说起:
1.constructor
:类组件初始化state时
1 | constructor(props) { |
替换成hooks写法,使用useState
:
1 | cosnt [uploading, setUploading] = useState(false) |
2.render
:类组件渲染时
1 | class ImgUplod extends PureComponent { |
替换成hooks写法,就是函数组件本身,直接使用return
:
1 | function ImgUplod () { |
3.componentDidMount
:类组件渲染完毕,即DOM加载完成时
有点像DOM的onLoad
方法,在挂载的时候执行,且只会执行一次,常用来做一些请求数据、事件监听等。
1 | componentDidMount() { |
替换成hooks写法,使用useEffect
:
1 | useEffect(() => { |
4.componentDidUpdate
:类组件重新渲染时
注意首次渲染是不会执行此方法的,可以获取到该组件上次的props或state。
也是react具有时间旅行
这一特点的原因。
1 | componentDidUpdate(preProps) { |
替换成hooks写法,使用useEffect
:
1 | useEffect(() => { |
准确来说,useEffect还是和componentDidUpdate有些区别。
componentDidUpdate是首次不执行,更新才执行;
useEffect是首次会执行,更新也执行。
注意:为什么componentDidUpdate有if判断,而useEffect没有?因为useEffect监听的就是this.props.data
这个数据,只有当data发生变化时,才会执行useEffect里面的内容(自动if)。
还有一个疑问,那么在hooks中如何获取历史props和state呢?
除了useEffect
,还需要和useRef
配合,来实现时间旅行:
1 | const preProps = useRef<number>() |
5.componentWillUnmount
:类组件销毁时
当路由跳转后,组件销毁时触发。
可以操作,如:移除事件监听、移除 localStorage 持久化数据等。
1 | componentWillUnmount() { |
替换成hooks写法,使用useEffect
:
1 | useEffect(() => { |
通过hooks的useEffect
、useState
、useRef
就能把react常用的生命周期都实现一遍,是不是很神奇?不常用的生命周期,如:getDerivedStateFromProps、shouldComponentUpdate、getSnapshotBeforeUpdate,在这里就不做考虑了。
函数组件useState:等同于类组件this.setState。
举例:
this.setState方式:
1 | class DeivceTest extends PureComponent { |
useState方式:
1 | const DeviceTest = () => { |
上面提到副作用
这一概念,实现副作用就是使用useEffect
。
那什么是副作用呢?除了数据渲染到视图外的操作,都可以是副作用
。
比如:发起网络请求,访问DOM元素,写本地持久化缓存、绑定解绑事件等。
副作用时机:
Mount之后(componentDidMount)、
Update之后(componentDidUpdate)、
Unmount之前(componentWillUnmount)
调用一次副作用:
1 | // 相当于,componentDidMount 和 componentWillUnmount |
调用多次副作用:
1 | // 相当于,componentDidMount 和 componentDidUpdate |
多说一下useEffect,毕竟它太重要了。
可以发现[]是useEffect的精髓所在
,正确的使用好useEffect,减少不必要的逻辑错误。
useEffect是在render之后
调用的,即组件DOM渲染完成之后调用。每个useEffect只处理一种副作用
。这种模式,就是关注点分离。不同的副作用,分开放!
useContext是为了解决props层层传递的问题,可以实现多层级的数据共享。
用法:
1.通过createConntext创建Context对象
1 | import { createContext } from 'react'; |
2.父组件:用Context.Provider包裹子组件,其包含的所有子组件共享该数据
1 | import Context from 'hooks/useContext' |
3.子组件:通过useContext获取父组件的值
1 | import Context from 'hooks/useContext' |
注意:该hook,尽量别乱用,因为会破坏组件的独立性
。
上一章提到过React.memo(),useMemo和memo对比就能知道其意义。
React.memo() 和 PureComponent,针对的是组件的渲染是否重复执行
。
useMemo,针对的是定义的函数逻辑是否重复执行
。
本质用的是同样的算法,判断依赖是否改变,进而决定是否触发特定逻辑。
输入输出是对等的,相同的输入一定产生相同的输出,就和数学的幂等
一样。
先用React.memo举例:
在父组件中,有两个子组件:
一个子组件export default React.memo(Child1)
,
另一个组件export default Child2
。
1 | function Parent() { |
当父组件的 current 发生变化时,子组件Child2会不停被渲染,尽管它没做任何的变化;而加了memo的子组件Child1只会被渲染一次。
再用useMemo举例:
子组件Child1:
1 | import React from 'react'; |
子组件Child2:
1 | import React, { useMemo } from 'react'; |
两者的区别在于:
子组件Child1,在render一次后,count * 2 是调用两次、执行两次计算
,一个是日志里的,一个是页面的;
而子组件Child2,在render一次后,count * 2是调用两次、执行一次计算
。
useMemo 相当于Vue中computed里的计算属性,当某个依赖项改变时才重新计算值,这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo作用:避免重复计算,减少资源浪费
。
useMemo和useEffect的调用时机不同:useMemo是在render之前,useEffect是在render之后。
useCallback,也是针对的是定义的函数逻辑是否重复执行
。
useMemo解决的是避免在每次渲染时都进行高开销的计算问题
。
useCallback解决的是传入组件的函数属性导致其他组件渲染问题
。
说的可能有点绕,下面来举例说明:
父组件:
1 | function Parent() { |
子组件Child2:
1 | import React from 'react'; |
尽管没有点击执行onClick事件,但是还是会让子组件Child2渲染,因为Child2的onClick函数属性,每次都会创造成新的函数。
我们改下onClick事件:
1 | // 改造前 |
改造后,不会执行Child2的任何渲染。
useMemo 和 useCallback 都是作性能优化之用,与业务逻辑无关
。
useRef 不仅仅是用来管理 DOM ref
的,它还相当于this
, 可以存放任何变量。不需要引起组件重新渲染的变量
,都可以放在ref里。
举例:管理DOM ref
1 | function Parent() { |
点击组件Child2,实现input聚焦。
再举例:存放任何变量
使用useRef存放渲染前一个的变量:
1 | const preProps = useRef<number>() |
使用useRef存放一个变量,阻止多次点击导致重复请求接口:
1 | const lock = useRef<boolean>(false) |
疯狂点击ing:
以上只是举例,按理说useRef还有更多的玩法。
还有一种阻止多次点击导致重复请求接口的方案,通过dva-loading 控制 disabled 属性,从而控制点击事件。
1 | (loading: loading.effects['login/addASync']) => { |
useState:数据渲染到组件的操作
useEffect:数据渲染到组件之外的操作
useContext:需要props多层传递的操作
useMemo:避免重复计算的操作
useCallback:避免组件函数属性引起渲染的操作
useRef:存放不需要引起组件渲染的变量
执行时机:
Context.Provider 是在render之前声明,useContext 是在render之后执行;
useState 是在render之前声明,setState 是在render之后执行;
useRef 是在render之前声明,ref.current 是在render之后执行;
useMemo、useCallback 是在render之前执行;
useEffect 是在redner之后执行;
简单理解:执行除了优化性能的hook在render之前执行;其余hook的使用都是render之后执行。
除了以上说到的常见6个hook之外,官方提到的hook还有useReducer
、useImperativeHandle
、useLayoutEffect
、useDebugValue
。不怎么常用,就不一一说明了。
react官方hook api:
https://reactjs.org/docs/hooks-reference.html
这个不太好定性,因为项目不同、业务不同,什么时候使用一时半会也说不清。
先重点说下为什么React会引入useMemo、useCallback这两个hook的原因?
原因一:引用不相等
原因二:重复计算
原因二重复计算在上一节介绍useMemo的时候说过,在这就不多说什么了。
下面重点说下原因一引用相等的问题。
如果你是编程人员,你很快就会明白为什么会这样:
1 | true === true // true |
当在React函数组件中定义一个对象时,尽管它跟上次定义的对象相同,引用是不一样的(即使它具有所有相同值和相同属性)
。
这会引起两个问题:
1.给组件添加行为事件和对象porps的时候
1 | function CountButton({onClick, count}) { |
因为每次函数引用都是不一样的,肯定会引起其他组件的渲染。
点击第一个按钮,会引起第二个按钮的渲染。
解决方案:React.memo 和 useMemo/useCallback 配合使用
1 | const CountButton = React.memo(function CountButton({onClick, count}) { |
2.使用useEffect时,[]传入的值为对象、数组、函数
1 | function Blub() { |
useEffect 将在每次渲染中对 arr、fun 进行引用相等性检查。
由于[]中传入的是数组、函数,尽管 arr、fun 里面的值不变,但每次渲染引用都是新的,所以还会执行useEffect的回调。
解决方案:useMemo/useCallback
1 | function Blub() { |
除了useEffect,同样的事情也适用于传递给 useLayoutEffect, useCallback, 和 useMemo 的依赖项。
最后总结一下什么时候useMemo、useCallback。
在解决重复计算
的场景上使用useMemo
;
在解决引用不相等
的场景上使用useCallback
。
通过自定义hook,可以将组件逻辑提取到可重用的函数中
。
一句话理解自定义hook:复用页面就组件化
、复用逻辑就自定义hook
。
下面来实现一个简单的自定义hook:
useTitle:设置当前页面标题
创建一个useTitle.tsx:
1 | import { useEffect } from 'react' |
在Home.tsx和Login.tsx中分别引入并使用该hook:
1 | import useTitle from 'hooks/useTitle' |
创建自定义 Hook 是不是特别简单呢。值得注意的是:1.自定义 Hook 必须以 “use” 开头
2.我们可以在一个组件中多次调用自定义hook,因为它们是完全独立的
3.hook本质就是函数
本篇先介绍到这里。下一篇将继续讲解electron+hooks+ts项目,尽情期待。(本篇重点react的hooks,下篇重点ts技术栈)