jtwang7 / React-Note

React 学习笔记
8 stars 2 forks source link

React - useLayoutEffect 和 useEffect 的执行时机 #18

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

React - useLayoutEffect 和 useEffect 的执行时机

参考文章: 深入理解 React useLayoutEffect 和 useEffect 的执行时机

React 官方解释

从 React 执行流程解释

render {创建React Element; diffing算法} -> pre-commit {getSnapshotBeforeUpdate; 注册 useEffect/useLayout 的回调函数} -> commit {Virtual DOM To DOM} {由于JS线程与浏览器渲染线程互斥,此处继续执行同步代码: 生命周期方法/useLayoutEffect此前注册的回调函数 } -> 渲染到浏览器视口 -> 渲染线程结束后,空闲阶段通知JS线程执行队列中的 useEffect 回调函数。
  1. 当 react 组件状态发生改变时,会触发 react diff,render() 会创建出一个新的 React Element,diffing 算法会比较新旧 React Element,并在 diff 结束后进入到 commit 阶段,准备把虚拟 DOM 发生的变化映射到真实 DOM 上。

  2. 在 commit 阶段的前期,会调用一些生命周期方法,对于类组件来说,需要触发组件的 getSnapshotBeforeUpdate 生命周期,对于函数组件,此时会调度 useEffect 的 create/destroy 函数。

    注意是此处是调度 useEffect 内注册的方法,不是执行。在这个阶段,会把使用了 useEffect 组件产生的生命周期函数入列到 React 自己维护的调度队列中,给予一个普通的优先级,让这些生命周期函数异步执行。 这里也可以看出 useEffect 和 useLayoutEffect 的一点区别:useEffect 是异步执行的。

  3. 随后,就到了 React 把虚拟 DOM 设置到真实 DOM 上的阶段,这个阶段主要调用的函数是 commitWork,commitWork 函数会针对不同的 fiber 节点调用不同的 DOM 的修改方法 (比如文本节点和元素节点的修改方法是不一样的) 对 DOM 节点进行更新。

    commit 阶段操作参考React-Fiber

commitWork 如果遇到了类组件的 fiber 节点,不会做任何操作,会直接 return,进行收尾工作,然后去处理下一个节点,这点很容易理解,类组件的 fiber 节点没有对应的真实 DOM 结构,所以就没有相关操作

  1. 函数组件在这个阶段,若存在 hooks ,则会同步调用上一次渲染时 useEffect/useLayoutEffect 注册的 create 函数所返回的 destroy 函数。注意,该阶段是在 commitWork 后发生的,这个时候,react 已经把发生的变化映射到真实 DOM 上了。 (重点来了!) 但由于 JS 线程和浏览器渲染线程是互斥的,因为 JS 虚拟机还在运行,即使内存中的真实 DOM 已经变化,浏览器也没有立刻渲染到屏幕上。此时会进行收尾工作,同步执行对应的生命周期方法,我们说的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函数都是在这个阶段被同步执行。
  2. 对于 react 来说,commit 阶段是不可打断的,会一次性把所有需要 commit 的节点全部 commit 完,至此 react 更新完毕,JS 停止执行,浏览器把发生变化的 DOM 渲染到屏幕上,到此为止 react 仅用一次回流、重绘的代价,就把所有需要更新的 DOM 节点全部更新完成。
  3. 浏览器渲染完成后,浏览器通知 react 自己处于空闲阶段,react 开始执行自己调度队列中的任务,此时才开始执行 useEffect(create, deps) 的产生函数。

    useEffect 产生函数的真正调用时机是在 requestIdleCallback 处。见React-Fiber

注意:useEffect 的异步执行主要是从人机交互方面体现的,假设 useEffect 是同步执行的,useEffect 中有一个数据请求的操作,那么就表明,在数据请求时,用户的一系列交互行为是无法被反馈的,这样会很给人不好的应用体验,你说对不对呢?

jtwang7 commented 2 years ago

Questions

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 进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗。