React Hooks 源码解读之 useEffect

845 阅读10分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

react 版本:v17.0.3

1、前言

React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:

  • 组件挂载阶段:

    useEffect = ReactCurrentDispatcher.current.useEffect = HooksDispatcherOnMount.useEffect = mountEffect;

  • 组件更新阶段:

    useEffect = ReactCurrentDispatcher.current.useEffect = HooksDispatcherOnUpdate.useEffect = updateEffect;

因此,组件在首次加载时,执行 useEffect,其实执行的是 mountEffet,而组件更新时,则执行的是updateEffect 。

2、挂载阶段

组件在挂载阶段,执行 useEffect,实际上执行的是 mountEffect,下面我们来这个函数的实现。

2.1 mountEffect

// packages/react-reconciler/src/ReactFiberHooks.new.js

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (
    __DEV__ &&
    enableStrictEffects &&
    (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
  ) {
    return mountEffectImpl(
      // | 位运算符,标识 fiber 节点的副作用异步执行的
      MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
      HookPassive,  // 标识 hook 为 useEffect
      create, // useEffect 第的第一个参数 callback
      deps,   // useEffect 的第二个可选参数 依赖项 []
    );
  } else {
    return mountEffectImpl(
      // | 位运算符,标识 fiber 节点的副作用
      PassiveEffect | PassiveStaticEffect,
      HookPassive,  // 标识 hook 为 useEffect 
      create, // useEffect 第的第一个参数 callback
      deps,   // useEffect 的第二个可选参数 依赖项 []
    );
  }
}

可以看到,mountEffect 接受 useEffect 传入的 回调函数(create) 和 依赖项{deps) 两个参数,并返回了 mountEffectImpl 的执行结果。在调用 mountEffectImpl 时传入了用于位运算的 fiber 节点标识和 hook 对象的标识,还传入了 useEffect提供的两个参数 callback 和依赖项。

在 mountEffect 函数中将 mountEffectImpl 的执行结果返回给了 useEffect,我们来看看 mountEffectImpl 做了什么事情。

2.2 mountEffectImpl

// packages/react-reconciler/src/ReactFiberHooks.new.js

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
  const hook = mountWorkInProgressHook();
  // 初始化 useEffect 的第二个参数 依赖项
  const nextDeps = deps === undefined ? null : deps;
  // 当前 fiber 节点的二进制值,区分当前 effect 是 useEffect 还是 useLayoutEffect
  currentlyRenderingFiber.flags |= fiberFlags;
  // 初始化 effect 链表,添加到 useEffect hook 的 memoizedState 属性上
  // 因此 useEffect hook 的 memoizedState 并不是一个具体的值(useState、useReducer 的 memoizedState 是一个具体的值),而是一个 effect 链表
  hook.memoizedState = pushEffect(
    // HookHasEffect 和 hookFlags 做位运算
    // HookHasEffect 标记Effect的回调和销毁函数需要执行
    // hookFlags 参数值为 HookPassive,表示 hook 是 useEffect
    HookHasEffect | hookFlags, 
    create, // useEffect hook 的第一个参数 callback
    undefined,
    nextDeps, // useEffect hook 的第二个参数 依赖项数组
  );
}

可以看到,在 mountEffectImpl 里,首先调用 mountWorkInProgressHook(),将当前的 hook 添加到 workInProgressHook 单向链表中,并返回最新的 hook 链表:

const hook = mountWorkInProgressHook();

接着初始化 useEffect 的第二个参数 依赖项数组:

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

再接下来将标识fiber节点的二进制值添加到当前fiber 节点的 flags 属性上:

currentlyRenderingFiber.flags |= fiberFlags;

最后,调用 pushEffect() ,初始化 effect 链表,将其添加到 useEffect hook 的 memoizedState 属性上:

hook.memoizedState = pushEffect(
  // HookHasEffect 和 hookFlags 做位运算
  // HookHasEffect 标记Effect的回调和销毁函数需要执行
  // hookFlags 参数值为 HookPassive,表示 hook 是 useEffect
  HookHasEffect | hookFlags, 
  create, // useEffect hook 的第一个参数 callback
  undefined,
  nextDeps, // useEffect hook 的第二个参数 依赖项数组
);

由此可知道, useEffect 的 memoizedState 并不是一个具体的值,而是一个 effect 链表。而 useState、useReducer 的 memoizedState 是一个具体的值。

2.3 mountWorkInProgressHook

在 mountEffectImpl() 函数中,使用 mountWorkInProgressHook() 函数创建了一个新的 hook 对象,我们来看看它是如何被创建的:

// packages/react-reconciler/src/ReactFiberHooks.new.js

// 创建一个新的 hook 对象,并返回当前的 workInProgressHook 对象
// workInProgressHook 对象是全局对象,在 mountWorkInProgressHook 中首次初始化
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };
  
   // Hooks are stored as a linked list on the fiber's memoizedState field
  // 将 新建的 hook 对象以链表的形式存储在当前的 fiber 节点memoizedState属性上

  // 只有在第一次打开页面的时候,workInProgressHook 为空
  if (workInProgressHook === null) {
    // This is the first hook in the list
    // 链表上的第一个 hook
    
    // currentlyRenderingFiber: The work-in-progress fiber. I've named it differently to distinguish it fromthe work-in-progress hook.
    
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    // 已经存在 workInProgressHook 对象,则将新创建的这个 Hook 接在 workInProgressHook 的尾部,形成链表
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

可以看到,在新建一个 hook 对象时,如果全局的 workInProgressHook 对象不存在 (值为 null),即组件在首次渲染时,将新建的 hook 对象赋值给 workInProgressHook 对象,同时将 hook 对象赋值给 currentlyRenderingFiber 的 memoizedState 属性,如果 workInProgressHook 对象已经存在,则将 hook 对象接在 workInProgressHook 的尾部,从而形成一个单向链表。

在 mountEffect 的最后,调用 pushEffect 初始化了hook 对象上的 effect 链表,并添加到 useEffect hook 的 memoizedState 属性上。接下来我们来看看 pushEffect 的实现。

2.4 pushEffect

// packages/react-reconciler/src/ReactFiberHooks.new.js

function pushEffect(tag, create, destroy, deps) {
  // 新建一个 effect 对象
  const effect: Effect = {
    tag, // effect的tag,是一个二进制值,用于区分useEffect和useLayoutEffect
    create, // useEffect 的第一个参数 callback
    destroy,
    deps, // useEffect 的 第二个参数  依赖项数组
    // Circular
    next: (null: any), // 链表的next指针,链接下一个 effect
  };
  // 从当前 Fiber 节点的 updateQueue 属性上获取当前 Fiber 节点的 更新队列
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);

  if (componentUpdateQueue === null) {
    // 如果当前 Fiber 节点的更新队列不存在,则创建一个更新队列
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    // 将 effect 链表添加到 更新队列上
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {

    // 当前 Fiber 节点上以存在更新队列,将当前的 effect 添加到 effect 链表的末尾
    // effect 是一个环形链表

    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;
    }
  }

  // 返回 effect 环形链表
  return effect;
}

首先,根据传进来的参数,创建一个 effect 对象,该 effect 对象上存储着 useEffect 的两个参数: callback 和 依赖项数组,还存储着用于标识 hook 对象的二进制数值,每个 hook 对象都是通过 next指针连接下一个 effect。

const effect: Effect = {
  tag, // 标识是useEffect还是useLayoutEffect(HasEffect、Layout、Passive )
  create, // useEffect 的第一个参数 callback
  destroy,
  deps, // useEffect 的 第二个参数  依赖项数组
  // Circular
  next: (null: any), // 链表的next指针,链接下一个 effect
};

然后判断当前 Fiber 节点是否已经存在更新队列,如果不存在,则新建一个更新队列,并将当前的 effect 添加到更新队列的 lastEffect 属性上,并将 effect 的 next 指向自己,形成环形链表。

  if (componentUpdateQueue === null) {
    // 如果当前 Fiber 节点的更新队列不存在,则创建一个更新队列
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    // 将 effect 链表添加到 更新队列上
    componentUpdateQueue.lastEffect = effect.next = effect;
  } 

如若更新队列已经存在,则将当前的effect添加到 effect 链表的末尾,并将当前的effect的next指向第一个effect,形成环形链表。

else {

  // 当前 Fiber 节点上以存在更新队列,将当前的 effect 添加到 effect 链表的末尾
  // effect 是一个环形链表

  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;
  }
}

最后是将 effect 返回给 mountEffectImpl,在 mountEffectImpl 中将 effect 赋值给 useEffect hook 的 memoizedState 属性。

这就是组件首次渲染时,useEffect 创建 effect 的整个过程,我们看下它的流程图:

3、更新阶段

接下来我们来看看更新过程中 useEffect 实际调用的方法 updateEffect。

3.1 updateEffect

// packages/react-reconciler/src/ReactFiberHooks.new.js

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  
  // 删除了 dev 代码
   
  // PassiveEffect: 二进制值,标识 fiber 节点的副作用是异步执行的(即使用的是useEffect,useEffect 是异步的)
  // HookPassive: 表示 hook 为 useEffect  
  // create: useEffect 第的第一个参数 callback
  // deps: useEffect 的第二个可选参数 依赖项 []
  // 比较前后的 deps ,判断是否需要重新执行useEffect的第一个参数 callback
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

可以看到,updateEffect 接受 useEffect 传入的 回调函数(create) 和 依赖项{deps) 两个参数,并返回了 updateEffectImpl 的执行结果。updateEffectImpl 通过比较前后的 deps (useEffect 的第二个参数) ,判断是否需要重新执行 useEffect 的 callback 参数,我们来看看 updateEffectImpl 的实现。

3.2 updateEffectImpl

// packages/react-reconciler/src/ReactFiberHooks.new.js

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 当前正在更新的 fiber 节点上的 hook
  const hook = updateWorkInProgressHook();
  // 新的 deps
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  //currentHook: 当前 fiber 节点上的 hook 对象

  // 当前 fiber 节点上存在 hook 对象
  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)) {
        // 新旧 deps 相同,传入 hookFlags, 表示不需要 update,更新 hook 对象上的 effect 链
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  // 代码执行到这里,表示新旧的 deps 不一样

  // 更新 当前 fiber 节点上的 flags 标识,区分当前 effect 是 useEffect 还是 useLayoutEffect
  currentlyRenderingFiber.flags |= fiberFlags;

  // 传入 HookHasEffect | hookFlags 位运算的结果,更新 hook 对象上的effect 链
  hook.memoizedState = pushEffect(
    // HookHasEffect 和 hookFlags 做位运算
    // HookHasEffect 标记Effect的回调和销毁函数需要执行
    // hookFlags 参数值为 HookPassive,表示 hook 是 useEffect
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

首先调用 updateWorkInProgressHook() ,获取当前正在执行 update 任务的fiber节点上的hook 对象:

const hook = updateWorkInProgressHook();

接着获取新的 deps,方便与旧的 deps 比较,从而决定是否需要更新:

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

然后从当前的 hook 对象上获取旧的 deps,调用 areHookInputsEqual() 来比较前后 deps 是否相同:

// 当前 fiber 节点上存在 hook 对象
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)) {
      // 新旧 deps 相同,传入 hookFlags, 表示不需要 update,更新 hook 对象上的 effect 链
      hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
      return;
    }
  }
}

如果 areHookInputsEqual() 返回的结果为 true,说明前后的 deps 是一样的,该 effect 没有产生副作用,则调用 pushEffect,传入hookFlags,表示不更新执行 useEffect 的callback,然后更新 effect 链。

如果 areHookInputsEqual() 返回的结果为 false,则会执行下面的语句:

  // 更新 当前 fiber 节点上的 flags 标识
  currentlyRenderingFiber.flags |= fiberFlags;

  // 传入 HookHasEffect | hookFlags 位运算的结果,更新 hook 对象上的effect 链
  hook.memoizedState = pushEffect(
    // HookHasEffect 和 hookFlags 做位运算
    // HookHasEffect 标记Effect的回调和销毁函数需要执行
    // hookFlags 参数值为 HookPassive,表示 hook 是 useEffect
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );

可见,updateEffectImpl 最主要的作用,就是通过比较前后的 deps,判断是否是需要重新执行 useEffect 的 callback。

3.3 updateWorkInProgressHook

在 updateEffectImpl() 函数中,通过 updateWorkInProgressHook() 函数获取到了当前正在工作中的 Hook,即 workInProgressHook,我们来看看 updateWorkInProgressHook 的实现:

// packages/react-reconciler/src/ReactFiberHooks.new.js

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.

  // 获取 当前 hook 的下一个 hook
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  // 取下一个 hook 为当前的hook
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.

    // 拷贝当前的 hook,作为当前正在工作中的 workInProgressHook

    invariant(
      nextCurrentHook !== null,
      'Rendered more hooks than during the previous render.',
    );
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      // This is the first hook in the list.
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

这里分两种情况:

  • 如果是在 render 阶段,则会取下一个 hook 作为当前的hook,并返回 workInProgressHook;
  • 如果是在 re-render 阶段,则在当前处理周期中,继续取当前的 workInProgressHook 做更新处理,最后再返回 workInProgressHook。

前后的 deps 是否一样,是通过 areHookInputsEqual() 来判断的,我们来看看它的实现。

3.4 areHookInputsEqual

// packages/react-reconciler/src/ReactFiberHooks.new.js

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
    
  // 删除了 dev 代码
    
  if (prevDeps === null) {
    
    // 删除了 dev 代码
    
    return false;
  }

  // 删除了 dev 代码
    
  // deps 是一个 Array,循环遍历去比较 array 中的每个 item
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // is比较函数是浅比较
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

在 areHookInputsEqual() 函数中,循环遍历 deps ,调用 is 方法去比较依赖项数组中的每个依赖,值得注意的是,is 方法是浅比较,也就是说如果是深比较那一定会更新的。下面是 is 方法的源码:

// packages/shared/objectIs.js

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs: (x: any, y: any) => boolean =
  typeof Object.is === 'function' ? Object.is : is;

export default objectIs;

若当前浏览器支持 Object.is() 方法,则调用该方法来判断两个值是否相同,若不支持,则调用React自己实现 is 方法来比较。

3.5 pushEffect

无论是否需要重新执行 useEffect 的 callback,最后都会调用 pushEffect 去更新 hook 对象上的 effect 链表,然后将更新后的 effect 添加到 hook 对象上的 memoizedState 属性上。pushEffect 在 「3.4 pushEffect」小节已经讲解过,这里不再赘述。

4、useEffect 流程图