深⼊理解ReactuseLayoutEffect和useEffect的执⾏时机
我们先看下 React 官⽅⽂档对这两个 hook 的介绍,建⽴个整体认识
useEffect(create, deps):
该 Hook 接收⼀个包含命令式、且可能有副作⽤代码的函数。在函数组件主体内(这⾥指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录⽇志以及执⾏其他包含副作⽤的操作都是不被允许的,因为这可能会产⽣莫名其妙的 bug 并破坏 UI 的⼀致性。使⽤ useEffect 完成副作⽤操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执⾏。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃⽣通道。
useLayoutEffect(create, deps):
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调⽤ effect。可以使⽤它来读取 DOM 布局并同步触发重渲
染。在浏览器执⾏绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
注意加粗的字段,React 官⽅的⽂档其实把两个 hook 的执⾏时机说的很清楚,下⾯我们深⼊到 react 的执⾏流程中来理解下
问题
useEffect 和 useLayoutEffect 的区别?
useEffect 和 useLayoutEffect 哪⼀个与 componentDidMount,componentDidUpdate 的是等价的?
useEffect 和 useLayoutEffect 哪⼀个与 componentWillUnmount 的是等价的?
为什么建议将修改 DOM 的操作⾥放到 useLayoutEffect ⾥,⽽不是 useEffect?
流程
1. react 在 diff 后,会进⼊到 commit 阶段,准备把虚拟 DOM 发⽣的变化映射到真实 DOM 上
2. 在 commit 阶段的前期,会调⽤⼀些⽣命周期⽅法,对于类组件来说,需要触发组件的 getSnapshotBeforeUpdate ⽣命周期,对于函
数组件,此时会调度 useEffect 的 create destroy 函数
3. 注意是调度,不是执⾏。在这个阶段,会把使⽤了 useEffect 组件产⽣的⽣命周期函数⼊列到 React ⾃⼰维护的调度队列中,给予⼀个
普通的优先级,让这些⽣命周期函数异步执⾏
// 可以近似的认为,React 做了这样⼀步,实际流程中要复杂的多
setTimeout(() => {
const preDestory = element.destroy;
if (!preDestory) prevDestroy();
const destroy = create();
element.destroy= destroy;
}, 0);
4. 随后,就到了 React 把虚拟 DOM 设置到真实 DOM 上的阶段,这个阶段主要调⽤的函数是 commitWork,commitWork 函数会针对不
同的 fiber 节点调⽤不同的 DOM 的修改⽅法,⽐如⽂本节点和元素节点的修改⽅法是不⼀样的。
5. commitWork 如果遇到了类组件的 fiber 节点,不会做任何操作,会直接 return,进⾏收尾⼯作,然后去处理下⼀个节点,这点很容易
理解,类组件的 fiber 节点没有对应的真实 DOM 结构,所以就没有相关操作
reacthooks理解
6. 但在有了 hooks 以后,函数组件在这个阶段,会同步调⽤上⼀次渲染时 useLayoutEffect(create, deps) create 函数返回的 destroy 函数
7. 注意⼀个节点在 commitWokr 后,这个时候,我们已经把发⽣的变化映射到真实 DOM 上了
8. 但由于 JS 线程和浏览器渲染线程是互斥的,因为 JS 虚拟机还在运⾏,即使内存中的真实 DOM 已经变化,浏览器也没有⽴刻渲染到
屏幕上
9. 此时会进⾏收尾⼯作,同步执⾏对应的⽣命周期⽅法,我们说的componentDidMount,componentDidUpdate 以及
useLayoutEffect(create, deps) 的 create 函数都是在这个阶段被同步执⾏。
10. 对于 react 来说,commit 阶段是不可打断的,会⼀次性把所有需要 commit 的节点全部 commit 完,⾄此 react 更新完毕,JS 停⽌执⾏
11. 浏览器把发⽣变化的 DOM 渲染到屏幕上,到此为⽌ react 仅⽤⼀次回流、重绘的代价,就把所有需要更新的 DOM 节点全部更新完成
12. 浏览器渲染完成后,浏览器通知 react ⾃⼰处于空闲阶段,react 开始执⾏⾃⼰调度队列中的任务,此时才开始执⾏ useEffect(create,
deps) 的产⽣的函数
⼏个问题
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 进⾏修改,会触发浏览器再次进⾏回流、重绘,增加了性能上的损耗。