ReactHooks源码解析之useEffect

May 31, 2022

前沿

举个例子来讲解下 React.useEffect():

import React, { useEffect } from "react"; import React from "react"; export default function App() { useEffect(() => { console.log("classComponent:componentDidMount"); return () => { console.log("classComponent:componentWillUnmount"); }; }, []); return <div>      a     </div>; }
javascript

当执行 App()时,会调用 useEffect(xxx),因为是 useEffect()的第一次调用,所以此时会执行源码里的 mountEffect()

一、mountEffect()

作用:

在 dev 下调试

执行 mountEffectImpl()

源码

//首次调用 React.useEffect 走这里 function mountEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null ): void { if (__DEV__) { //删除了 dev 代码 } return mountEffectImpl( //逻辑或,即 是 UpdateEffect+PassiveEffect UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, //create 也就是 useEffect的第一个参数 callback create, //useEffect 的第二个可选参数 [] deps ); }
javascript

当执行 App()时,会调用 useEffect(xxx),因为是 useEffect()的第一次调用,所以此时会执行源码里的 mountEffect

解析:

可以看到,如果不用 dev 调试的话,直接调 mountEffectImpl()就可以了

二、mountEffectImpl()

作用:

将当前 hook 加入 workInProgressHook 链表中

初始化 effect 链并赋值给 hook.memoizedState

作用:

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { //将当前 hook 加入 workInProgressHook 链表中,并返回最新的 hook 链表 //关于mountWorkInProgressHook()的讲解,请看: // [ReactHooks源码解析之useState及为什么useState要按顺序执行] // (https://juejin.cn/post/6844904152712085512)中的「一、mountState()解析(1)」 const hook = mountWorkInProgressHook(); //初始化 deps 参数 const nextDeps = deps === undefined ? null : deps; //将 sideEffectTag 置为 fiberEffectTag(因为sideEffectTag=0) sideEffectTag |= fiberEffectTag; //初始化 effect 链并返回 //useEffect hook 的 memoizedState 并不是一个具体的值,而是一个 effect 对象 hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps); }
javascript

解析:

注意下传入的参数:

fiberEffectTag:UpdateEffect | PassiveEffect

hookEffectTag:UnmountPassive | MountPassive

create:useEffect 的第一个参数 callback,在「前言」的例子中,也就是:

() => { console.log("classComponent:componentDidMount"); return () => { console.log("classComponent:componentWillUnmount"); }; };
javascript
  1. deps:useEffect 的第二个参数 依赖数组,在例子中是:[ ]

调用 mountWorkInProgressHook(),将当前 hook 加入 workInProgressHook 链表中,并返回最新的 hook 链表

const hook = mountWorkInProgressHook();
javascript

关于 mountWorkInProgressHook()的讲解,请看:

ReactHooks 源码解析之 useState 及为什么 useState 要按顺序执行中的 「 一、mountState()解析(1) 」 (3) 初始化 deps 参数

const nextDeps = deps === undefined ? null : deps;
javascript

(4) 将 sideEffectTag 置为 fiberEffectTag

sideEffectTag |= fiberEffectTag;
javascript

(5) 初始化 effect 对象并返回

hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
javascript

useState 的 hook.memoizedState 是设置的值,而 useEffect 的 hook.memoizedState 是一个对象,也就是 effect 对象 接下来我们看下 pushEffect 里做了什么

三、pushEffect()

作用:

(1) 初始化 effect 对象并返回

(2) 将 effect 对象添加至更新队列 componentUpdateQueue 末尾

源码:

function pushEffect(tag, create, destroy, deps) { const effect: Effect = { tag, create, destroy, deps, // Circular next: (null: any), }; //如果 FunctionComponent 的更新队列不存在的话,则初始化它 if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); componentUpdateQueue.lastEffect = effect.next = effect; } //否则将此 effect 添加至更新队列末尾 else { const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { componentUpdateQueue.lastEffect = effect.next = effect; } else { const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
javascript

解析:

因为 ReactHooks 是给 FunctionComponent 提供副作用的函数,也就是说一定是有一个地方来存放 FunctionComponent 的副作用的,那么在源码里就是 componentUpdateQueue 链表来存放副作用的

如果 FunctionComponent 的更新队列不存在,则调用 createFunctionComponentUpdateQueue()来创建一个更新队列,并将该 useEffect 的 effect 对象放至更新队列队尾

if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); componentUpdateQueue.lastEffect = effect.next = effect; }
javascript

补充:

createFunctionComponentUpdateQueue()的源码:

//创建 FunctionComponent 的更新队列 function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue { return { lastEffect: null, }; }
javascript

(3) 如果 FunctionComponent 的更新队列存在的话,则将此 effect 添加至更新队列末尾

 else {     const lastEffect = componentUpdateQueue.lastEffect;     if (lastEffect === null) {       componentUpdateQueue.lastEffect = effect.next = effect;     } else {       const firstEffect = lastEffect.next;       lastEffect.next = effect;       effect.next = firstEffect;       componentUpdateQueue.lastEffect = effect;     }   }
javascript

综上,可以看到当第一次调用 useEffect 时,React 做了 3 件事:

① 将当前 hook 加入 workInProgressHook 链表中

② 初始化 effect 对象

③ 将 effect 对象加入 componentUpdateQueue 更新队列(FunctionComponent 存放副作用的链表)队尾

④ 将 effect 对象赋值给 hook.memoizedState

以上是在 render 阶段完成的,接下来会在 commit 执行该 effect

四、mountEffect()执行的时机

如果你看过中的 React 源码解析之 Commit 第一子阶段「before mutation」中的「三、commitHookEffectList()」的话,那么就会明白上文的 effect.create:

effect.create = () => { console.log("classComponent:componentDidMount"); return () => { console.log("classComponent:componentWillUnmount"); }; };
javascript

会在 commitHookEffectList()中执行:

function commitHookEffectList( unmountTag: number, mountTag: number, finishedWork: Fiber ) { //... if ((effect.tag & mountTag) !== NoHookEffect) { // Mount const create = effect.create; effect.destroy = create(); } }
javascript

此时的 effect.tag=UnmountPassive | MountPassive:

export const MountPassive = /*         */ 0b01000000; //64 export const UnmountPassive = /*       */ 0b10000000; //128
javascript

也就是 effect.tag=192

只有当传进 commitHookEffectList()的 mountTag 为 MountPassive 或者是 UnmountPassive,才会执行 effect.create()

那么 React 是在什么时候调用 commitHookEffectList()时传入 MountPassive|UnmountPassive 呢?

调用顺序如下(均在 commit 阶段):commitRootImpl()——>flushPassiveEffects()——>commitPassiveHookEffects(effect)——>commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork)&commitHookEffectList(NoHookEffect, MountPassive, finishedWork)

补充:

关于 commitRootImpl()的讲解,请看:

(React 源码解析之 commitRoot 整体流程概览) 接下来我们看下 flushPassiveEffects()主要做了什么

五、flushPassiveEffects()

作用:

清除 effect 上的副作用

核心源码:

export function flushPassiveEffects() { //effect 链表上第一个有副作用的 fiber ////比如在 app() 中调用了 useEffect() let effect = root.current.firstEffect; while (effect !== null) { if (__DEV__) { //删除了 dev 代码 } else { try { //执行 fiber 上的副作用 commitPassiveHookEffects(effect); } catch (error) { invariant(effect !== null, "Should be working on an effect."); captureCommitPhaseError(effect, error); } } effect = effect.nextEffect; } }
javascript

解析:

循环执行 effect 链上的副作用(side-effect)

六、commitPassiveHookEffects()

作用:

执行 fiber 上的副作用

源码:

//执行 fiber 上的副作用 export function commitPassiveHookEffects(finishedWork: Fiber): void { commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork); commitHookEffectList(NoHookEffect, MountPassive, finishedWork); }
javascript

解析:

主要是调用 commitHookEffectList()函数,但要注意下传的参数:

① 第一次调用先传的是 UnmountPassive,那么就会执行 effect.destory()方法, 对应到开发层面,就是当多次更新调用 useEffect 时,会先执行上个 useEffect 的 return 回调函数:

useEffect(() => { console.log("classComponent:componentDidMount"); //执行这个 return 的 callback return () => { console.log("classComponent:componentWillUnmount"); }; }, []);
javascript

② 第二次调用传的是 MountPassive,那么就会执行 effect.create()方法,对应到开发层面, 就是执行 useEffect 的第一个参数 callback:

useEffect( //执行这个 callback () => { console.log("classComponent:componentDidMount"); return () => { console.log("classComponent:componentWillUnmount"); }; }, [] );
javascript

这也就解释了调用 useEffect 为什么会先执行上个 useEffect 的 return 回调函数? 这个问题

七、commitHookEffectList()

作用:

循环 FunctionComponent 上的 effect 链,根据 hooks 上每个 effect 上的 effectTag,执行 destroy/create 操作(类似于 componentDidMount/componentWillUnmount)

源码:

function commitHookEffectList( unmountTag: number, mountTag: number, finishedWork: Fiber ) { // FunctionComponent 的更新队列 // 补充:FunctionComponent的 side-effect 是放在 updateQueue.lastEffect 上的 // ReactFiberHooks.js中的pushEffect()里有说明: // componentUpdateQueue.lastEffect = effect.next = effect; const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; //如果有副作用 side-effect的话,循环effect 链,根据 effectTag,执行每个 effect if (lastEffect !== null) { //第一个副作用 const firstEffect = lastEffect.next; let effect = firstEffect; do { //如果包含 unmountTag 这个 effectTag的话,执行destroy(),并将effect.destroy置为 undefined //NoHookEffect即NoEffect if ((effect.tag & unmountTag) !== NoHookEffect) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } } //如果包含 mountTag 这个 effectTag 的话,执行 create() if ((effect.tag & mountTag) !== NoHookEffect) { // Mount const create = effect.create; effect.destroy = create(); if (__DEV__) { //删除了 dev 代码 } } effect = effect.next; } while (effect !== firstEffect); } }
javascript

解析:

① 主要参考之前写的文章——(React 源码解析之 Commit 第一子阶段「before mutation」 「 三、commitHookEffectList() 」)

② 注意下 lastEffect 是怎么取到的:

root.current.firstEffect.updateQueue.lastEffect;
javascript

当前 fiber 节点上的 effect 链上第一个有 side-effect 的 effect 的更新队列上的最新的 lastEffect

③ 对应到开发层面上,当 App() 第一次调用 useEffect 时,React 创建 App() 的 effect 链,并将 lastEffect.destory 赋为 undefined,那么就不会执行 destory()了 但是会执行 lastEffect.create(),打印出'classComponent:componentDidMount' 那么,App()第一次调用 useEffect 的源码解析流程就结束了,接下来看下多次调用 useEffect 的流程

八、updateEffect()

作用:

多次调用 useEffect 时,调用的函数

源码:

//多次更新时,走这里 function updateEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null ): void { if (__DEV__) { //删除了 dev 代码 } return updateEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps ); }
javascript

解析:

注意下 updateEffectImpl()传的参数,跟二、mountEffectImpl()中传的参数一样

九、updateEffectImpl()

作用:

比较 deps 判断是否需要重新执行 useEffect 的 callback

源码:

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { // 当前正在 update 的 fiber 上的 hook const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined; //currentHook:当前 fiber 对象上的 hook 对象 //当currentHook不为空时 if (currentHook !== null) { //获取旧 effect 状态 const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; //如果 deps 参数存在的话 if (nextDeps !== null) { //获取旧 deps 参数 const prevDeps = prevEffect.deps; //比较前后 deps 是否相同 if (areHookInputsEqual(nextDeps, prevDeps)) { //如果相同的话,则表示没有 update,那么就传入 NoHookEffect tag pushEffect(NoHookEffect, create, destroy, nextDeps); //return 代表下面的代码都不执行了 return; } } } //能执行到这里,说明currentHook=null 或者 deps 有 update //那么就添加 UpdateEffectTag sideEffectTag |= fiberEffectTag; //初始化 effect 链并返回 hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps); }
javascript

解析:

(1) 执行 updateWorkInProgressHook(),获取当前正在 update 的 fiber 上的 hook

const hook = updateWorkInProgressHook();
javascript

(2) 获取 deps,方便与 prevDeps 比较,来决定是否更新

const nextDeps = deps === undefined ? null : deps;
javascript

(3) 然后就是调用核心函数 areHookInputsEqual(),比较前后 deps 是否相同

if (currentHook !== null) { //获取旧 effect 状态 const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; //如果 deps 参数存在的话 if (nextDeps !== null) { //获取旧 deps 参数 const prevDeps = prevEffect.deps; //比较前后 deps 是否相同 if (areHookInputsEqual(nextDeps, prevDeps)) { //如果相同的话,则表示没有 update,那么就传入 NoHookEffect tag pushEffect(NoHookEffect, create, destroy, nextDeps); //return 代表下面的代码都不执行了 return; } } }
javascript

如果 areHookInputsEqual()返回的结果为 true 的话,说明该 effect 没有产生副作用,则为该 effect 添加 NoHookEffect 的 effectTag 表示不更新执行 useEffect 的 callback,并返回

(4) areHookInputsEqual()源码

function areHookInputsEqual( nextDeps: Array<mixed>, prevDeps: Array<mixed> | null ) { //删除了 dev 代码 if (prevDeps === null) { //删除了 dev 代码 return false; } //删除了 dev 代码 for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; } return true; }
javascript

因为 deps 是一个 Array,所以会循环去比较 array 中的每个 item

注意:

这里是用 Object.is() 去进行浅比较的,也就是说深比较是一定会更新的 (5) Object.is()源码

function is(x, y) { return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); }
javascript

(6) 如果 areHookInputsEqual()返回为 false 的话,就会执行下面的语句

//能执行到这里,说明currentHook=null 或者 deps 有 update //那么就添加 UpdateEffectTag sideEffectTag |= fiberEffectTag; //初始化 effect 链并返回 hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
javascript

(7) 那么就会再次调用 commitPassiveHookEffects()——>commitHookEffectList()

注意:

多次调用同一个 useEffect 的时候,会先去执行上一次的 destory(),再执行本次的 create()