对于useEffect,我们通常会见到如下问题:
- 如何用 useEffect 模拟生命周期?
- 如何在 useEffect 里请求数据?
- 函数可不可以当 useEffect 的依赖?
- 为什么使用 useEffect 的时候会出现死循环?
- 为什么有时候在 useEffect 中拿到的 state 或 props 是旧的?
关于useEffect和生命周期
我们理解 useEffect 总是把他和 class 的一些生命周期比较, 但其实这样的想法是不对的, 我们思考的正确方式应该是基于状态修改而非生命周期。
react的状态变化的底层逻辑就是:state和props变化了,UI视图就要发生改变。
class生命周期关注的是:在什么时期,对数据进行什么操作。
useEffect-hooks关心的是:当数据改变时候,应该进行什么操作
一、useEffect
1. 为什么要使用useEffect?
useEffect 钩子允许在功能组件中执行副作用。 在功能组件的主体(即 React 渲染阶段)中不允许使用突变、订阅、定时器、日志记录和其他副作用。这可能会导致混乱的错误和用户界面的不一致。 因此,建议使用 useEffect。传递给 useEffect 的函数将在呈现提交到屏幕后执行,或者如果您将依赖关系数组作为第二个参数传递,那么每当其中一个依赖关系发生变化时,该函数就会被调用。
2. useEffect 作用
2.1 模拟生命周期
useEffect可以模拟class类三个生命周期:组件挂载后,组件更新,组件卸载
| 组件生命周期 | useEffec设置 |
|---|---|
| 组件挂载后-componentDidMount | 设置第二个参数:useEffect的依赖项为空数组 |
| 组件更新-componentDidUpdate | 设置第二个参数:监听对应的数据变量 |
| 组件更新&&挂载(前两者) | 设置第二个参数:null |
| 组件卸载-componentWillUnmount | 设置第一个参数:返回一个函数设置了;第二个参数:空数组。 因为返回的那个函数会在组件卸载前调用,所以相当于componentWillUnMount |
2.2 处理副作用
二、useLayoutEffect
commit 分三个阶段:
- BeforeMutation。
- Mutation:在这里更新 DOM。
- Layout。
commitRootImpl 中的三个函数的调用分别对应这个三个阶段:
因为在layout后面执行的useEffect,所以hook的名称为「useLayoutEffect」。
useLayoutEffect触发时机
从上面图解我们可以看出,浏览器重新绘制之前应该是生成等待绘制的真实DOM的时间点,那就说明useLayoutEffect触发是在生成了真实并且等待绘制的真实dom之后触发的。官网上有说,我们需要注意的是useLayoutEffect是同步执行,会阻塞浏览器重新绘制,所以在生成真实dom之后,React会开始触发需要执行的useLayoutEffect钩子,等完全执行完所有useLayoutEffect之后才会把真实dom交给渲染引擎进行绘制。由于不管是useLayoutEffect还是useEffect钩子,都是js代码,所以都将在js引擎中执行,我们之前也解释了同步执行的逻辑,因此总结:useLayoutEffect会在生成真实dom之后,根据声明顺序同步执行,图解表示如下:
useEffect触发时机
根据官网定义,useEffect触发时机是依赖变更重新渲染后异步执行,那什么时候才算是重新渲染后呢,实际是当我们js引擎把生成的真实dom交给渲染引擎之后,就开始执行useEffect钩子了,在js引擎生成真实dom之前,会把所有的useEffect钩子放入队列中等待执行,当把真实dom交给渲染引擎之后,会把所有useEffect钩子函数按顺序依次取出执行,并且每个useEffect钩子执行是异步的,也就是并行的,相互之间并不会影响,每个useEffect钩子函数各自执行。图解表示如下
为什么useEffect中无法获取到最新dom元素,而useLayoutEffect可以
我们从上述对useEffect和useLayoutEffect触发时机可以很清楚的看出,useLayoutEffect会在useEffect之前执行,那为什么useLayoutEffect能够获取到最新dom元素而useEffect却不能呢,那这个时候就必须要解释一下浏览器对于处理获取元素的方法处理方式了,实际上浏览器对于处理获取元素的方法处理方式有两种
- 从刚刚生成还未来得及绘制的真实 DOM 树上获取: 当我们浏览器中存在最新的等待绘制的真实dom时,我们调用获取元素方法时,浏览器会将等待绘制的真实dom上的元素返回给我们
- 从已经绘制完成或正在绘制的页面上获取: 当我们浏览器中不存在最新的等待绘制的真实dom时,我们调用获取元素方法时,浏览器会将已经完成绘制或者正在绘制的页面上的元素返回给我们
上面的两种获取dom元素的处理方式中:真实dom优先级 > 已绘制的dom。
当useLayoutEffect中获取dom元素时,浏览器会将js引擎中生成的等待绘制的真实dom元素返回,此时的dom元素就是最新的元素(浏览器目前只生成了真实dom,且还没教给GUI渲染引擎还没开始,所以只能从真实dom中拿)。
当useEffect中获取dom元素时,已经到了重新绘制的阶段(这个时候真实都dom已经教给GUI渲染引擎了,无法从js引擎中拿到真实dom,所以只能等绘制好之后从绘制的元素中拿)。但是js的执行速度是高于页面的绘制速度的,加上另一方面,下一帧页面正在绘制过程中,此时页面展示的是老页面,且js已经开始执行了,所以useEffect是拿不到最新的DOM值。
虽然理论上将useEffect函数的执行和页面的绘制是同时进行的,但是实际上,JavaScript执行速度和页面绘制速度之间的差距可以是相当大的,所以即便两者是同时开始的,但是实际计算机内部执行速度来看,基本上绘制刚刚开始,useEffect函数都已经执行完了,所以在useEffect回调函数中获取元素,几乎不可能获取到最新的元素。
两个钩子分别在什么时候使用呢? 一般来说,使用大多数处理副作用的场景都是在useEffect中,而如果发现屏幕初始化后有更新了(即视觉上出现一闪烁的效果),此时可以考虑是否需要替换为useLayoutEffect。
参考文章:
juejin.cn/post/725925…
developer.aliyun.com/article/912…
juejin.cn/post/728923…
juejin.cn/post/725925…