React-Hook 最佳实践
React Hook 新出现背景
类组件的问题
复用组件状态难,高阶组件+渲染属性
providers customers,等一堆工具都是为了解决这个问题,但是造成了很严重的理解成本和组件嵌套地狱生命周期带来的负面影响,逻辑拆分严重
This 的指向问题
函数组件的局限
之前函数组件没有
state和 生命周期,导致使用场景有限
React Hook
Hooks 是 React 16.8 新增的特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,无需转化成类组件
Hook 的使用和实践
useState 和 Hook 的闭包机制
快速点击下的情况下,想想 Hook 组件和函数式组件控制台打印出来的是什么?
类组件打印出来的是
3 3 3Class组件的state是不可变的,通过setState返回一个新的引用,this.state指向一个新的引用setTimeout执行的时候,通过this获取最新的state引用,所以这个输出都是3函数组件打印的结果是
0 1 2函数组件闭包机制,函数组件每一次渲染都有独立的
props和state每一次渲染都有独立的事件处理函数
每一次渲染的状态不会受到后面事件处理的影响
函数组件渲染拆解
既然每次渲染都是一个独立的闭包,可以尝试代码拆解函数式组件的渲染过程
三次点击,共
4次渲染,count从0变为3页面第一次渲染,页面看到的
count = 0第一次点击,事件处理器获取的
count = 0,count变成1, 第二次渲染,渲染后页面看到count = 1,对应上述代码第一次点击第二次点击,事件处理器获取的
count = 1,count变成2, 第三次渲染,渲染后页面看到count = 2,对应上述代码第二次点击第三次点击,事件处理器获取的
count = 2,count变成3, 第四次渲染,渲染后页面看到count = 3,对应上述代码第三次点击
让函数式组件也可以输出 3 3 3
有种比较简单并且能解决问题的方案,借用 useRef
useRef返回一个可变的ref对象,其current属性被初始化为传入的参数(initialValue)useRef返回的ref对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref对象都是同一个useRef可以类比成类组件实例化后的this,在组件没有销毁的返回的引用都是同一个
这样修改一下,控制台输出的确实是
3 3 3? 既然
Ref对象整个生命周期都不变,修改current属性也只是修改属性,那除了打印,这里的You clicked 0 times,点击三次,会变成3么?显然不能,这个组件没有任何的属性和状态改变,会重新渲染才怪,所以这里虽然点击了
3次,但是不会像useState一样,渲染4次,这里只会渲染1次,然后看到的都是You clicked 0 times修复一个问题把另外一个更大的问题引进来,这很程序员。。。
参考 React 实战视频讲解:进入学习
useEffect
通过 useRef 虽然能解决打印的问题,但是页面渲染是不对的,这里还是使用 useState 的方案,配合 useEffect 可以实现我们想要的效果
看下
useEffect的签名,effect是函数类型,并且必填, 还有第二个可选参数,类型是只读数组useEffect是处理副作用的,其执行时机在 每次Render渲染完毕后,换句话说就是每次渲染都会执行,在真实DOM操作完毕后。
配合这个 hook, 如果每次 state 改变后渲染完之后,把 ref 里面的值更新,然后控制台打印 ref 的值,
这样子写可以符合我们的预期效果,页面展示从 0 1 2 3, 然后控制台输出 3 3 3,然后我们拆解下渲染过程。
三次点击,共
4次渲染,count从0变为3页面初始化渲染,
count = 0,currentCount.current = 0, 页面显示0, 渲染完成,触发useEffect,currentCount.current = 0第一次点击,
count = 0, 渲染完成后,count = 1, 页面显示1,触发useEffect,currentCount.current = 1第二次点击,
count = 1, 渲染完成后,count = 2, 页面显示2,触发useEffect,currentCount.current = 2第三次点击,
count = 2, 渲染完成后,count = 3, 页面显示3,触发useEffect,currentCount.current = 3三次点击完成,
currentCount.current = 3,第四次渲染,页面看到count = 3,setTimeout中调用的是currentCount这个对象,输出都是3
useEffect 的函数返回值
useEffect 的回调函数可以返回空,也可以返回一个函数,如果返回一个函数的话,在 effect 执行回调函数的时候,会先执行上一次 effect 回调函数返回的函数
这个 useEffect ,每次渲染完之后,控制台会先输出 last time effect return,然后再输出 after render
useEffect 和 类组件生命周期
之前提到,useEffct 有两个参数,第二参数是个可选参数,是 effect 的依赖列表, React 根据这些列表的值是否有改变,决定渲染完之后,是否执行这个副作用的回调
如果不传这个参数,React 会认为这个 effect 每次渲染然之后都要执行,等同于 componentDidUpdate 这个生命周期无约束执行
如果这个参数是空数组,React 会认为组件内任何状态和属性改变,都不会触发这个 effect,相当于这个 effect 是仅仅在组件渲染完之后,执行一次,后面组件任何更新都不会触发这个 effect,等同 componentDidMount
如果配合 useEffect 回调函数的返回函数,可以实现类似 componentWillUnmount 的效果,因为如果是空数组的话,组件任何更新都不会触发 effect,所以回调函数的返回函数只能在组件销毁的时候执行
如果依赖列表里面有值,则类似componentDidMount有条件约束更新,只有当上一次的状态和这次的不一样,才执行
useEffect 和 闭包问题
假设组件需要在初始化的时候,定义一个定时器,让 count 自增,自然而然的可以写出以下的代码
但是实际运行的时候,类组件展示是对的,函数组件从 0 递增到 1 之后,页面渲染就再也不变了
之前有提过,类组件因为有
this这个引用,很容易通过state拿到最新的值函数组件每次渲染都是独立的闭包,这里因为写的依赖值是
[],所以只有首次渲染后,才会这行这个effect,首次渲染后,count就是0,所以setCount(count + 1)每次都是执行setCount(0 + 1),所以定时器工作是正常的,不过取的值有问题。
闭包问题的切入点和发生场景
闭包问题,大多发生在,有些回调函数执行,依赖到组件的某些状态,但是这些状态并没有写到 useEffect 的依赖列表里面。导致执行回调函数的时候,拿到组件的状态不是最新的。
主要的场景有:
定时器
事件监听的回调
各种
Observer的回调
这些场景,通常只要在组件初始化渲染完之后,定义一次回调函数就好,但是如果回调函数依赖到组件的转态或者属性,这时候就要小心,闭包问题
例如这里的写法,在组件渲染完监听 hashchange ,回调函数是拿不到后续更新的 state 的,只能能到初始化时候的空字符串。
尝试解决闭包问题-监听state变化
既然回调函数要每次都拿到最新的 state,可以监听 state 的变化,state 变化的时候,重新定义事件监听器,改写一下
以上代码能用,但是 state 每次改变,就会重新定义一个 hashchange 回调函数,但是上一次的 hashchange 的事件监听器并没有清除,代码能跑,但是内存泄漏也太严重了,可以配合 useEffect 回调函数返回的函数配合清掉上一次的事件监听器
这样内存泄漏的问题被解决了,但是这种事情监听,正常来说设置一次就好,没必要重新定义,还有别的更好的方法么?
尝试解决闭包问题 - setState 另外一种更新组件状态的方式
useState 返回的更新状态的函数,除了可以传一个值,还可以传一个回调函数,回调函数带一个参数,这个参数是最新的 state,像这样的话,之前那个定时器的例子,可以修改成这样。
这里我们改了一行代码,setCount(count + 1) 改成了 setCount((c) => c + 1),这样修改之后,其实定时器回调已经没有依赖到 count 这个值了,由 setCount 里面的回调函数,返回最新的 count 的值,就是setCount((c) => c + 1),里面的 c.
同样的,对于事件监听器里面,我们也可以通过这个方式去获取最新的 state,但是这里有几个问题
这个回调函数,其实也只要获取最新的
state,所以在调用setState的时候,拿到最新的值的同时,记得把setState的值,设置成和当前同一个,如果没有返回,那调用setState之后,state的值会变成undefinedsetState返回一个同样的值,会不会导致组件和它的子组件重新渲染?找了下文档说明是这样的:调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心看起来可行的,做一下简单的修改其实可以改成这样
这样基本就没问题了,做到了只定义了一次回调,然后也可以获取最新的 state,一举两得,但是还是有问题的
setState回调函数如果不写return stateCallback;这段代码,会导致state莫名其妙被设置成undefined,而且非常不好发现,维护性太差setState是用来改变组件的state的,不是让你这样用的的,虽然这样用完全没问题。但是可维护性太差了,如果你的代码被接手,别人就会疑惑这里为什么要这么写,无注释和变量命名太糟糕的情况下,代码可以维护性基本为0设置一个同样的
state,虽然不会导致子组件重新渲染,但是本组件还是有可能重新渲染的,按官网的说法
这个方案不完美。思路再发散一下?执行回调函数的时候,需要获取到最新的 state,能不能用一个不变的值缓存 state ? 等等?? 不变的值???
解决闭包问题最佳实践-useState和useRef
useRef的返回是在整个组件生命周期都是不变的一个对象,可以借助 useRef 来获得最新的 state。例如这个例子可以改成这样:
stateRef.current 上面两种写法,都可以获得最新的 count,回调函数里面里面直接读取 stateRef.current 的值,可以拿到最新的 state 闭包问题的最优解,节本就是这样了。
useRef 和 useState 的最佳实践
useState和useRef仔细想想和和类组件的什么属相很相似?是不是和this.state和this的属性很像在类组件中,如果是不参渲染的属性,直接挂
this上就好了,如果需要参与渲染的属性,挂在this.state上同样的,在
Hook中,useRef和useState可以实现类似效果
例如以下的例子
再看看 useEffect 回调函数的返回值
确定是没有返回或者返回一个函数,所以下面这种写法是有问题的,虽然也没有明显标明返回体,就是没有返回一样,但是这个回调函数是异步函数,异步返回默认返回一个 Promise 对象,所以这种写法是不提倡的
为了规避这个问题,可以修改一下写法
useCallback
把函数写进里面没什么问题,官方也推荐,但是万一我的副作用里面需要处理多个函数或者一个超长的函数的话,一个是不美观,一个是太难维护
这个适用可以利用 useCallback 把函数抽离出去,useCallback 返回一个记忆化的函数,当且仅当依赖列表有任何属性改变的时候,它才会返回一个新的函数,所以这个特性比较适合传给子组件的回调函数
这里如果 count 改变的时候,getFetchUrl的值也会改变,从而导致 useEffect 也触发
React.memo
React.memo() 返回一个记忆化的值,如果 React 内部会判定,如果重新渲染 props` 不相等,就会重新渲染,如果没有改变,就不会触发组件渲染
这个特性比较有用,因为如果父组件重新渲染的时候,子组件就会重新渲染,使用这个特性可以减少不必要的子组件重新渲染
React.useCallback 和 React.memo
为什么讲
useCallback要把memo拎出来讲,想一下useCallback的作用,返回一个缓存的函数,在函数组件里面,每次渲染都会执行一次组件函数,组件函数每次执行,在组件内部的函数都会重新定义,这样的话,父组件传给子组件的回调函数每次渲染都会变再从
memo的角度去看,父组件每次渲染,子函数组件如果不加memo的话,就算是子组件无任何依赖,属性都不变的情况下,子组件也会重新渲染如果在父组件单独加为子组件的回调函数添加
useCallback,这样可以避免回调函数重新定义,但是子组件如果不用memo包裹,就算任何子组件属性没改变,还是会导致子组件重新渲染;同样的,如果子组件单独用
memo包裹,父组件每次渲染,重新定义回调函数,还是会导致重新所以,
memo和useCallback必须都用上,不然是没用的,不仅达不到优化的效果,而且会加重 React 比较的负担。要不就别用,要不就都用上。
React.useCallback 和 React.memo 最佳实践
父组件用 useCallback 包裹函数,子组件用 memo 包裹组件,要不就都不用
Raect.memo 的局限
React.memo包裹在组件上,可以对传给组件的属性进行判定,父组件导致子组件重新渲染的时候,memo包裹的组件,会判定属性是否和上次渲染时候否改变,如果有改变,子组件重新渲染,否则不会重新渲染。React.memo有个局限,只能防止来源于外部的属性,如果是来源于内部的属性,React.memo是无作用的,例如通过useContext直接注入组件内部的属性,它没法防止,可以看下下面这个简单的例子
上面的组件,
count或者step任意这个属性改变,都会导致两个子组件重新渲染,这显然是不对的。
React.useMemo 代替 React.momo
useMemo 和 memo 一样,返回一个记忆化的值,如果依赖项没有改变,会返回上一次渲染的结果,它和 useCallback 的差别就在一个是返回记忆化的函数,一个是返回记忆化的值,如果 useMemo 的回调函数执行返回一个函数,那它的效果和 useCallback 是一样的。
因而上面的组件可以改一下,下面这种写法就可以防止任意一个属性改变会导致两个子组件重新渲染的问题
React.momo 和 React.useMemo
React.momo在防止子组件重新渲染方面,是最简单的,在类组件里面有个React.PureComponent,其作用也是。但是它无法检测函数内部的状态变化,并且防止重新渲染,例如useContext注入的状态。不过它自动比较全部属性,使用起来方面。React.memo按照依赖列表是否有属性改变,决定是否返回新的值,一定程度上和Vue的计算属性类似,但是需要说动声明依赖的属性。相比React.momo,它的控制的粒度更细,但是一般的外部属性变化,用这个明显没有React.memo方便
useReducer useContext
useReducer是useState的一种替代方案,useState的内部实现就是useReducer它接收两个参数,和
redux一样,一个是reducer, 一个是初始值,有两个返回,一直是当前的state,一个是dispatch通过
dispatch调用action就可以修改state里面的数据本质的作用是,让数据和函数组件解耦,让函数组件只要发出
Action,就可以修改数据,由于数据不在组件内部,也不用处理内部state变化带来的effect。useContext和useReducer结合,一定程度上可以实现一个React Redux
其他 Hook
useImperativeHandle,搭配useRef和forwardRefs可以实现定制父组件可以引用子组件的属性和方法,而不是直接引用整个子组件的实例,在父组件需要调用子组件属性和方法,但是又不想全部属性和方法都给父组件调用的时候使用useLayoutEffect使用的不多,作用和useEffect一样,但是这个hook是在组件变化后,DOM节点生成后,渲染之前调用,区别于useEffect是渲染之后调用,不太推荐使用,会阻塞渲染useDebugValue可用于在React开发者工具中显示自定义hook的标签。类似Vue组件用的name或者React组件中的displayName,不影响代码运行
组件复用
React Hook 有自定义 Hook,React 类组件有高阶组件或者渲染属性
有个比较常见的场景,进入页面需要调用后端接口的问题,如果每个组件都写一次,很繁琐,假设处理数据的接口长这样子
高阶组件
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。这种组件复用还挺常见的,比如 React-redux 里面的 connect,React Router 的 withRouter
它可以做到:
属性代理,比如多个组件都使用到的公共属性,注入属性
包裹组件,比如将组件包裹在写好的容器里面
渲染挟持,比如权限控制
用处
代码复用
性能监测 打点
权限控制,按照不懂的权限等级,渲染不同的页面
高阶组件编写和使用
按上面请求的需求,做一个组件渲染完之后,就立即开始请求初始数据
使用方面,高阶组件可以修饰类组件或者函数组件
自定义 Hook 的编写和使用
自定义 Hook 的编写,一个简单的数据请求的 Hook
自定义 hook 只能在函数式组件使用,不能在类组件里面用
函数式式组件和类组件默认属性
类组件的问题被解决了么?
复用组件状态逻辑难
依赖自定义的 Hook,可以解决组件状态和逻辑复用的问题,但是自定义
Hook编写需要对Hook运行机制非常了解,门槛并不比高阶组件低
生命周期带来的负面影响,逻辑拆分严重
生命周期拆分逻辑的问题,在
Hook里面切实被解决了,不会存在同一个逻辑被拆分在N个生命周期里面了
This 的指向问题
这个问题在 Hook 里面也是解决了,因为函数没有
this,就不会有this的问题,但是相对的,如果需要一个不变的对象,请使用useRef
简单总结
useState可以实现类似state和setState的效果useEffect可以实现componentDidMountcomponentDidUpdatecomponentWillUnmount这几个生命周期的功能,并且写法更加简单,在每次渲染后都会触发,触发的条件是依赖项有改变useRef返回一个引用,每次渲染都返回同一个对象,和类组件this属性一致useCallback返回一个记忆化的回调函数,在依赖项改变的时候,回调函数会修改,否则返回之前的回调函数,对于一些需要传给子组件的函数,可以使用这个,避免子组件因为回调函数改变而改变useMemo返回一个记忆化的值,依赖项改变,返回的值才会变,可用来记忆化值,和Vue计算属性类似,避免重复计算,避免重复渲染自定义的
Hook是实现状态和逻辑复用,作用和高阶组件还有渲染属性差不多useReducer是useState的底层实现,可以管理多个state,把state从组件内部抽离出来useContext可以实现批量传值,注入多个组件,和useReduceruseMemo使用可以实现Redux的功能
使用感受
个人使用方面
函数式组件本身写起来就比类组件少写不少代码
闭包问题很影响开发和调试,提高了不少调试成本,如果不熟悉闭包机制,很难发现问题。
Hook中的闭包问题,大多还是由于依赖项没有填写导致闭包带来的问题,比类组件
This的更加恼人,主要调试不好发现问题,填不填依赖项也是一个让人纠结的活Hook的依赖不能自动识别,必须手动声明,虽然有插件辅助添加,但是使用起来还是不如Vue的Hook在熟悉
Hook的机制的情况下,Hook开发体验还是比类组件好很多
团队协作方面
其实在推广
Hook的时候,团队成员的Hook水平是不太一致的,很多人员就遇到了闭包问题,还有依赖死循环的问题,这个可能大大小小都遇到过,就好像上面提到的,解决闭包问题,方式五花八门,其实也是我自己摸索过来的,然后看到团队成员其实差不多还使用者state更新之后,重新设置监听的方式,这个并不是太好,只能说闭包问题解决了相对的,
React官方也没有总结太多最佳实践,很多都靠自己实践过来的,所以团队成员在刚接触Hook的时候,都是useEffectuseState两把API,甚至在React Hook的官方文档里面 Hook 简介,对于这两个Hook介绍的很多但对于其他常用的
Hook,比如useRef和useCallback使用场景其实没有太好的例子去支撑这些API的使用。倒是其实团队里面不少成员,面对着不参与渲染的属性,也是用useState,而不是使用useRef。就是很多新人接触Hook容易犯的一个错误。有不少同学有些插件没有装上,导致
React自动检测依赖项的插件没有生效,这无疑会给本身就难以发现的闭包问题加了一层霜所以我也定期在团队里面分享我认为是比较好的实践,去引导团队里面的同学
对于不喜欢用
React Hook的同学,直接用类组件,类组件虽然代码写起来繁琐,但是起码没有闭包这些问题,而且代码被接手之后容易读懂,React Hook只是一个工具,会使用会给你加分,但是不会使用只会用类组件,也不会对其他人代码有影响,比较类组件和函数组件是可以共存的









评论