关于useEffect的一切

时间:2022-07-24
本文章向大家介绍关于useEffect的一切,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

作为React开发者,你能答上如下两个问题么:

  1. 对于如下函数组件:
function Child() {
  useEffect(() => {
    console.log('child');
  }, [])

  return <p>hello</p>;
}

function Parent() {
  useEffect(() => {
    console.log('parent');
  }, [])

  return <Child/>;
}

function App() {
  useEffect(() => {
    console.log('app');
  }, [])

  return <Parent/>;
}

渲染<App/>时控制台的打印顺序是?

  1. 如下两个回调函数的调用时机相同么?
// componentDidMount生命周期钩子
class App extends React.Component {
  componentDidMount() {
    console.log('hello');
  }
}

// 依赖为[]的useEffect
useEffect(() => {
  console.log('hello');
}, [])

答案:

?向右滑动翻看答案                                                                     1. child -> parent -> app
                                                                                    2. 不同                                              

其实,这两个问题分别考察的是:

  • useEffect的执行顺序
  • useEffect如何介入React工作流程

本文接下来将深入源码,带你了解这些知识。

这,就是关于useEffect的一切。

useEffect的执行顺序

React的源码可以拆分为三块:

  • 调度器:调度更新
  • 协调器:决定更新的内容
  • 渲染器:将更新的内容渲染到视图中

其中,只有渲染器会执行渲染视图操作。

对于浏览器环境来说,只有渲染器会执行类似appendChildinsertBefore这样的DOM操作。

协调器如何决定更新的内容呢?

答案是:他会为需要更新的内容对应的fiber(可以理解为虚拟DOM)打上标记。

这些被打标记的fiber会形成一条链表effectList

渲染器会遍历effectList,执行标记对应的操作。

  • 比如Placement标记对应插入DOM
  • 比如Update标记对应更新DOM属性

useEffect也遵循同样的工作原理:

  1. 触发更新时,FunctionComponent被执行,执行到useEffect时会判断他的第二个参数deps是否有变化。
  2. 如果deps变化,则useEffect对应FunctionComponentfiber会被打上Passive(即:需要执行useEffect)的标记。
  3. 渲染器中,遍历effectList过程中遍历到该fiber时,发现Passive标记,则依次执行该useEffectdestroy(即useEffect回调函数的返回值函数)与create(即useEffect回调函数)。

其中,前两步发生在协调器中。

所以,effectList构建的顺序就是useEffect的执行顺序。

effectList

协调器的工作流程是使用遍历实现的递归。所以可以分为两个阶段。

我们知道,是从根节点向下一直到叶子节点,是从叶子节点一路向上到根节点。

effectList的构建发生在阶段。所以,effectList的顺序也是从叶子节点一路向上。

useEffect对应fiber作为effectList中的一个节点,他的调用逻辑也遵循的流程。

现在,我们有充足的知识回答第一个问题:

由于阶段是从ChildParentApp,所以相应effectList也是同样的顺序。

所以useEffect回调函数执行也是同样的顺序。

不要用生命周期钩子类比hook

我们在初学hook时,会用ClassComponent的生命周期钩子类比hook的执行时机。

即使官网也是这样教学的。

但是,从上文我们已经知道,React的执行遵循:

调度 -- 协调 -- 渲染

渲染相关工作原理是按照:

构建effectList -- 遍历effectList执行对应操作

整个过程都和生命周期钩子没有关系。

事实上生命周期钩子只是附着在这一流程上的钩子函数。

所以,更好的方式是从React运行流程来理解useEffect的执行时机。

渲染

按照流程,effectList会在渲染器中被处理。

对于useEffect来说,遍历effectList时,会找到的所有包含Passive标记的fiber

依次执行对应useEffectdestroy

所有destroy执行完后,再依次执行所有create

整个过程是在页面渲染后异步执行的。

回答第二个问题:

如果useEffectdeps[],由于deps不会改变,对应fiber只会在mount时被标记Passive

这点是类似componentDidMount的。

但是,处理Passive effect是在渲染完成后异步执行,而componentDidMount是在渲染完成后同步执行,所以他们是不同的。

useEffect与useLayoutEffect

componentDidMount更类似的是useLayoutEffect,他会在渲染完成后同步执行。

这里提供个在线Demo[1],你可以将Demo中的useLayoutEffect替换为useEffect,看看他们的区别。

总结

通过本文,我们了解了useEffect的完整执行过程。

本系列文章接下来会继续以实例 + 源码的方式,解读业务中经常使用的React特性。

参考资料

[1]

在线Demo: https://code.h5jun.com/haxufe/edit?js,output