一、官方解读
1.1 useEffect
该Hook
接收一个包含命令式、且可能有副作用代码的函数。在函数组件主体内(这里指在 React
渲染阶段)改变 DOM
、添加订阅
、设置定时器
、记录日志
以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug
并破坏 UI
的一致性。使用 useEffect
完成副作用操作。赋值给 useEffec
t 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
1.2 useLayoutEffect
其函数签名与 useEffect
相同,但它会在所有的 DOM
变更之后同步调用 effect
。可以使用它来读取 DOM
布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
二、调用流程
react
在diff
后,会进入到commit
阶段,准备把虚拟DOM
发生的变化映射到真实DOM
上- 在
commit
阶段的前期,会调用一些生命周期方法,对于类组件来说,需要触发组件的getSnapshotBeforeUpdate
生命周期,对于函数组件,此时会调度useEffect
的create destroy
函数 - 注意是调度,不是执行。在这个阶段,会把使用了
useEffect
组件产生的生命周期函数入列到React
自己维护的调度队列中,给予一个普通的优先级,让这些生命周期函数异步执行
// 可以近似的认为,React 做了这样一步,实际流程中要复杂的多
setTimeout(() => {
const preDestory = element.destroy;
if (!preDestory) prevDestroy();
const destroy = create();
element.destroy= destroy;
}, 0);
- 随后,就到了
React
把虚拟DOM
设置到真实DOM
上的阶段,这个阶段主要调用的函数是commitWork
,commitWork
函数会针对不同的 fiber 节点调用不同的DOM
的修改方法,比如文本节点和元素节点的修改方法是不一样的 commitWork
如果遇到了类组件的fiber
节点,不会做任何操作,会直接return
,进行收尾工作,然后去处理下一个节点,这点很容易理解,类组件的 fiber 节点没有对应的真实DOM
结构,所以就没有相关操作- 但在有了
hooks
以后,函数组件在这个阶段,会同步调用上一次渲染时useLayoutEffect(create, deps) create
函数返回的destroy
函数 - 注意一个节点在
commitWokr
后,这个时候,我们已经把发生的变化映射到真实DOM
上了 - 但由于
JS
线程和浏览器渲染线程是互斥的,因为JS
虚拟机还在运行,即使内存中的真实DOM
已经变化,浏览器也没有立刻渲染到屏幕上 - 此时会进行收尾工作,同步执行对应的生命周期方法,我们说的
componentDidMount
,componentDidUpdate
以及useLayoutEffect(create, deps)
的create
函数都是在这个阶段被同步执行。 - 对于
react
来说,commit
阶段是不可打断的,会一次性把所有需要commit
的节点全部commit
完,至此react
更新完毕,JS
停止执行 - 浏览器把发生变化的
DOM
渲染到屏幕上,到此为止react
仅用一次回流、重绘的代价,就把所有需要更新的DOM
节点全部更新完成 - 浏览器渲染完成后,浏览器通知
react
自己处于空闲阶段,react
开始执行自己调度队列中的任务,此时才开始执行useEffect(create, deps)
的产生的函数
三、问题
问题1:useEffect 和 useLayoutEffect 的区别?
useEffect
在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。useLayoutEffect
在渲染时是同步执行,其执行时机与componentDidMount
,componentDidUpdate
一致
问题二:对于 useEffect 和 useLayoutEffect 哪一个与 componentDidMount,componentDidUpdate 的是等价的?
useLayoutEffect
,因为从源码中调用的位置来看,useLayoutEffect
的create
函数的调用位置、时机都和componentDidMount
,componentDidUpdate
一致,且都是被 React 同步调用,都会阻塞浏览器渲染。
问题三: useEffect 和 useLayoutEffect 哪一个与 componentWillUnmount 的是等价的?
- 同上,
useLayoutEffect
的detroy
函数的调用位置、时机与componentWillUnmount
一致,且都是同步调用。useEffect
的detroy
函数从调用时机上来看,更像是componentDidUnmount
(注意React
中并没有这个生命周期函数)。
问题四:为什么建议将修改 DOM 的操作里放到 useLayoutEffect 里,而不是 useEffect?
- 可以看到在流程
9/10
期间,DOM
已经被修改,但浏览器渲染线程依旧处于被阻塞阶段,所以还没有发生回流、重绘过程。由于内存中的DOM
已经被修改,通过useLayoutEffect
可以拿到最新的DOM
节点,并且在此时对DOM
进行样式上的修改,假设修改了元素的height
,这些修改会在步骤11
和react
做出的更改一起被一次性渲染到屏幕上,依旧只有一次回流、重绘的代价。
如果放在useEffect
里,useEffect
的函数会在组件渲染到屏幕之后执行,此时对DOM
进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗。
四、参考
- https://www.cnblogs.com/iheyunfei/p/13065047.html