深入了解useEffect_深入了解一下
文章前部分总结于Youtobe-Lama Dev老哥的视频,视频讲的不错,总结成文章学习一下,后续又参考官网及其他资料进行了扩展(顺便提一下,官网真的是常看常新,总能 get 到新的东西)。
带着这些问题深入了解 useEffect
- 什么时候运行?
- 依赖关系如何运作?
- 原始依赖项和非原始依赖项有什么区别?
- 什么时候应该使用清理功能?
从一个最简单的 demo 开始(??注意:初始的 useEffect 没有依赖项):
1. 运行时机
结论:useEffect是在组件渲染之后再执行的。
在 React 官网中渲染和提交章节有说到:组件显示到屏幕之前,其必须被 React 渲染。所以 React 中组件的渲染的过程包含了 3 个主要因素:组件、React本身和浏览器。
官网很形象的将 React 组件的渲染用点餐的形式来体现:
如果简单组件是上述点餐过程,那么包含 useEffect 的组件就是我们去点堂食的同时,又点了一份打包的场景:优先制作堂食,之后制作打包的内容。
渲染过程:
- 步骤 1:触发渲染
应用启动时,会进行组件的初次渲染,此时count 值为0。包含 useEffect 的组件除了告诉 React 要呈现组件内容外,还要告诉 React 渲染完组件后要执行 effect:
- 步骤 2:React 渲染组件
接着,React 会调用组件来确定要在屏幕上显示的内容,之后提交给浏览器进行DOM 渲染。
- 第三步,执行 effect
浏览器又获取到信息要执行 effect的指令,并将其显示在屏幕上:
2. 添加依赖
对代码进行改造,添加一个 state ,并通过输入框改变它:
添加一个输入框之后,每次输入内容 useEffect 里面的代码都会执行:
但这并不是我想要的,我只想在 count 改变的时候才去执行 useEffect 里面的代码,这个时候就需要给 useEffect 添加一个依赖:
3. 处理非原始类型的依赖
在JavaScript中数据分为 原始类型 和 非原始类型 ,原始类型包括: number、 string 、 boolean 、 null 和 undefined 、 symbol,非原始类型 array 、 object 和 function我们知道在JS中非原始类型的数据即便值一样,也不相等:
在上述 useEffect 例子中使用的是原始类型的依赖,并没有什么问题,但是当设置为一个非原始类型的依赖时要注意:可能会导致一些不必要的渲染。这是因为:
- React 使用 Object.is 来比较每个依赖项和它先前的值。
- 在 JavaScript 中,每个新创建的对象和函数都被认为与其他所有对象和函数不同。即使他们的值相同:
这就是为什么应该尽可能避免将对象和函数作为 useEffect 的依赖。当出现这种情况时,应该尝试将它们
- 移到组件外部
- 或 Effect 内部
- 或从中提取原始值的方式
改造上述示例代码,将依赖改为原始值:
其他官方示例:
- 将静态对象和函数移出组件
- 移除 Effect 依赖
4. 使用 cleanup 清理功能
4.1. 日常使用 cleanup 的场景是清除定时器:
4.2. ??在useEffect中使用请求,应该使用cleanup来取消请求
这是日常开发中经常忽略的点,其实官方文档已经说明了:
注意,ignore 变量被初始化为 false,并且在 cleanup 中被设置为 true。这样可以确保 你的代码不会受到“竞争条件”的影响:网络响应可能会与你的发送不同的顺序到达。
如下例是经常遇到的场景,快速切换路由,在网速正常的情况下,好像没什么问题,但是网速慢的情况下,切换至下个路由时,总会闪现一下上一个路由的数据信息:
这个时候就需要使用 cleanup 来取消 state 的更新
在快速切换路由,2??3时,没有再显示2的信息,直接展示了3的内容:
上述取消,只是取消了状态的更新,并未真正取消接口的请求,在实际开发中可以根据使用的请求插件,直接取消请求,例如示例中使用的是axios,就可以基于原生的 AbortController 对象来取消请求:
直接取消了2的请求,展示3请求的内容
5. ??不要滥用useEffect
5.1. 使用自定义 Hook 复用逻辑
- 提取自定义 Hook 让数据流清晰,让进出 Effect 的数据流非常清晰。
- 你让组件专注于目标,而不是 Effect 的准确实现。
- 把你的 Effect 包裹进自定义 Hook,当这些解决方案可用时升级代码会更加容易。
- 当 React 增加新特性时,你可以在不修改任何组件的情况下移除这些 Effect。
- 这会让你的组件代码专注于目标,并且避免经常写原始 Effect。
5.2. 提取请求逻辑到自定义 hook 中
除上述取消请求的方式应该是所有请求都具备的之外,我们常用的还有这种请求场景:某个请求依赖某个 state,当 state 变化时,会进行请求。这两种场景都推荐使用自定义 hook 的方式来实现,因为把 Effect 包裹进自定义 Hook 可以更准确表达你的目标以及数据在里面是如何流动的。
官网提供了一种开发中最为常见的场景为案例:
一个请求显示城市列表,另一个显示选中城市的区域列表,如下代码是常见的处理方式:
而更优的处理方式应该是把请求方式抽离为一个自定义 hook:
在组件中复用:
5.3. 基于已有的 props 或 state 计算得出的值,不要把它作为一个 state
5.3.1. 简单计算得出的值,在渲染期间直接计算这个值
如果代码中某个 state 是依据 props 或其他 state 在 useEffect 中计算得出的值,那么可以考虑优化它,官网给出的一个极为简单却能很好说明问题的案例:
5.3.2. 数据庞大或复杂计算得出的值,使用useMemo进行缓存
上述例子是一个简单的计算,如果某个值的“计算”相当复杂或数据庞大,每次的渲染都会是很大的开销时,考虑使用 useMemo 来进行缓存,官网给的案例中,getFilteredTodos()的参数可能数据量很大,这时候使用useMemo对计算结果进行缓存,只有当todos或filter发生变化时,才会重新执行getFilteredTodos()
5.4. useEffect移除原则
5.4.1. 移除根据 props 或 state 来更新 state 的 useEffect,尽可能在渲染计算该值而不是作为一个 state。因为这样会导致不必要的渲染。
- 移除根据 props 变化时重置所有 state 的 useEffect,而是通过为组件添加一个 key ,来重置整个组件树的 state。
- 移除根据 props 变化时重置部分 state 的 useEffect,而是在渲染过程中计算该值:
->
- 可以使用useMemo缓存复杂的计算值。
5.4.2. 移除可以通过在事件处理函数中调整 state 的 useEffect,而是把逻辑放入事件处理函数中去。
- 如果某个逻辑是由某个特定的交互引起的,请将它保留在相应的事件处理函数中。如果是由用户在屏幕上 看到 组件时引起的,请将它保留在 Effect 中。
5.4.3. 移除链式计算,而是在渲染过程或事件处理函数中调整 state
常见于级联场景,某个值的变化会导致二级值变化,进而导致三级值变化:
->
?? 无法 在事件处理函数中直接计算出下一个 state的场景除外。如一个具有多个下拉菜单的表单,如果下一个下拉菜单的选项取决于前一个下拉菜单选择的值。这时,Effect 链是合适的,因为你需要与网络进行同步。
5.4.4. 避免错误的数据流向,React中,数据流是从父组件流向子组件的。
当组件中出现子组件更新父组件状态时,要考虑是否可以“状态提升”,避免子组件更新父组件的状态。
原文链接:
https://juejin.cn/post/7409828788127350793