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

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

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

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

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

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

比如 Placement 标记对应插入 DOM

比如 Update 标记对应更新 DOM 属性

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

触发更新时, FunctionComponent 被执行,执行到 useEffect 时会判断他的第二个参数 deps 是否有变化。

如果 deps 变化,则 useEffect 对应 FunctionComponent fiber 会被打上 Passive (即:需要执行useEffect)的标记。

渲染器 中,遍历 effectList 过程中遍历到该 fiber 时,发现 Passive 标记,则依次执行该 useEffect destroy (即 useEffect 回调函数的返回值函数)与 create (即 useEffect 回调函数)。

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

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

effectList

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

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

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

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

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

由于 阶段是从 Child Parent App ,所以相应 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,你可以将Demo中的useLayoutEffect替换为useEffect,看看他们的区别。

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

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