[React 源码] useEffect | useLayoutEffect [2.4k 字 - 阅读时长5min]

2,750 阅读7分钟

代码都来自 React 18 源码, 大家可以放心食用

useEffect 原理

问题: 下面这段代码,从挂载到更新发生了什么?怎么挂载的?怎么更新的。

function Counter() {
  useEffect(() => {
    console.log("useEffect1");
    return () => {
      console.log("destroy useEffect1");
    };
  });
  useEffect(() => {
    console.log("useEffect2");
    return () => {
      console.log("destroy useEffect2");
    };
  });
   useEffect(() => {
    console.log("useEffect3");
    return () => {
      console.log("destroy useEffect3");
    };
  });
  return <div></div>;
}

这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点


useEffect mount 挂载阶段

第一:判断是函数节点的 tag 之后,调用 renderWithHooks.

/* 
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate 
*/
let value = renderWithHooks(_current,workInProgress,Component);

第二:在 renderWithHooks 当中调用 Counter 函数

let children = Component();

第三: 调用 Counter 函数 的 useEffect 函数

export function useEffect(create, deps) {
  return ReactCurrentDispatcher.current.useEffect(create, deps);
}

第四:挂载阶段 ReactCurrentDispatcher.current.useEffect 实则是调用了 mountEffect, moutEffect 调用了 mountEffectImpl

return mountEffectImpl(
           UpdateEffect | PassiveEffect,
           HookPassive,
           create,
           deps
       );

第五:在 mountEffectImpl 中创建 调用 mountWorkInProgressHookpushEffect 方法

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps
  );
}

mountWorkInProgressHook 函数,创建 useEffectHook 对象,构建 fiber.memoizedState 也就是 Hook 链表

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

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

pushEffect 函数, 创建 effect 对象 ,赋值给 hook.memoizedState,构建 fiber.updateQueue队列,队列当中就是一个一个的 effect 对象。

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = 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;
}

前五步已经初始化 mount 完毕,第二个 和 第三个 都是按照此流程,得到了以下的结果。

image.png

第六:等到 renderer 阶段中的 beforeMutaion 阶段,将 flushPassiveEffectsImpl 异步调度

function performSyncWorkOnRoot(root) {
  flushPassiveEffects();
  commitRoot(
    root,
    workInProgressRootRecoverableErrors,
    workInProgressTransitions,
  );
  return null;
}

flushPassiveEffectsImpl函数:

function flushPassiveEffectsImpl() {
  commitPassiveUnmountEffects(root.current);
  commitPassiveMountEffects(root, root.current, lanes, transitions);
  return true;
}

第七:在 renderer 阶段完成之后(Dom 已经完成挂载),flushPassiveEffectsImpl函数 中 调用 commitPassiveUnmountEffects commitPassiveMountEffects,经过一系列的函数调用到达 commitHookEffectListMount, commitHookEffectListUnmount 执行传入 useEffectmount 函数和 返回的 unMount 函数。(挂载时 unMount 函数为 null)

commitHookEffectListMount函数: 遍历 fiber.updateQueue 以此执行传入 useEffectcreate 函数

function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
        effect.destroy = create();
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

commitHookEffectListUnmount 函数,执行 返回的 unMount 函数

function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        // 在此函数中 调用 distroy
        safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

自此 useEffect 挂载阶段执行完毕


useEffect update 更新阶段

第一:判断是函数节点的 tag 之后,调用 renderWithHooks.

/* 
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate 
*/
let value = renderWithHooks(_current,workInProgress,Component);

第二:在 renderWithHooks 当中调用 Counter 函数

let children = Component();

第三: 调用 Counter 函数的第一个 useEffect 函数

export function useEffect(create, deps) {
  return ReactCurrentDispatcher.current.useEffect(create, deps);
}

第四:更新阶段 ReactCurrentDispatcher.current.useEffect 实则是调用了 updateEffect, moutEffect 调用了 updateEffectImpl

return updateEffectImpl(
           UpdateEffect | PassiveEffect,
           HookPassive,
           create,
           deps
       );

第五:updateEffectImpl updateWorkInProgressHook 复用 currentHook 的属性 创建新的 Hook 对象 以及 新的 Hook 链表, 调用 areHookInputsEqual 函数判断依赖是否发生变化,没有变化直接 return ,有变化调用 pushEffects

function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return;
      }
    }
  }
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

pushEffect 函数 ,创建 effect 对象 ,赋值给 hook.memoizedState,构建 fiber.updateQueue队列,队列当中就是一个一个的 effect 对象。

第六:等到 renderer 阶段中的 beforeMutaion 阶段(commitRoot 之前),将 flushPassiveEffectsImpl 异步调度

function performSyncWorkOnRoot(root) {
  flushPassiveEffects();
  commitRoot(
    root,
    workInProgressRootRecoverableErrors,
    workInProgressTransitions,
  );
  return null;
}

flushPassiveEffectsImpl函数:

function flushPassiveEffectsImpl() {
  commitPassiveUnmountEffects(root.current);
  commitPassiveMountEffects(root, root.current, lanes, transitions);
  return true;
}

第七:在 renderer 阶段完成之后(layout 阶段结束之后),flushPassiveEffectsImpl函数 中 调用 commitPassiveUnmountEffects commitPassiveMountEffects,经过一系列的函数调用到达 commitHookEffectListMount, commitHookEffectListUnmount 执行传入 useEffectmount 函数和 返回的 unMount 函数。(更新的时候执行的 destroy 函数是上一次的 useEffect 的返回的函数)

    // 更新时候 调用 pushEffects  destroy 已经有值,是上一次的 useEffect 的返回的函数
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;

commitHookEffectListMount函数: 遍历 fiber.updateQueue 以此执行传入 useEffectcreate 函数

function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
        effect.destroy = create();
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

commitHookEffectListUnmount 函数,执行 返回的 unMount 函数

function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        // 在此函数中 调用 distroy
        safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

自此 useEffect 更新完毕


useLayoutEffect 原理

useLayoutEffect mount 挂载阶段

第一:判断是函数节点的 tag 之后,调用 renderWithHooks.

/* 
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate 
*/
let value = renderWithHooks(_current,workInProgress,Component);

第二:在 renderWithHooks 当中调用 Counter 函数

let children = Component();

第三: 调用 Counter 函数的第一个 useLayoutEffect 函数

export function useLayoutEffect(create, deps) {
  return ReactCurrentDispatcher.current.useLayoutEffect(create, deps);
}

第四:挂载阶段 ReactCurrentDispatcher.current.useEffect 实则是调用了 mountLayoutEffect, mountLayoutEffect 调用了 mountLayoutEffect

第五:在 mountEffectImpl 中创建 调用 mountWorkInProgressHookpushEffect 方法

  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );

mountWorkInProgressHook 函数,创建 useLayoutEffectHook 对象,构建 fiber.memoizedState 也就是 Hook 链表

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

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

pushEffect 函数, 创建 effect 对象 ,赋值给 hook.memoizedState,构建 fiber.updateQueue队列,队列当中就是一个一个的 effect 对象。

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = 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;
}

第六:等到 renderer 阶段中的 Mutation(Dom 挂载中阶段) 阶段,同步调用 layoutEffect 的 destroy 函数。

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
        // Layout effects are destroyed during the mutation phase so that all
        // destroy functions for all fibers are called before any create functions.
        // This prevents sibling component effects from interfering with each other,
        // e.g. a destroy function in one component should never override a ref set
        // by a create function in another component during the same commit.
        if (shouldProfile(finishedWork)) {
          try {
            startLayoutEffectTimer();
            // 同步调用 layoutEffect 的 destroy 函数
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
          recordLayoutEffectDuration(finishedWork);
        } else {
          try {
            // 同步调用 layoutEffect 的 destroy 函数
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
}

第七:等到 renderer 阶段中的 Layout (Dom 挂载后) 阶段,同步调用 useLayoutEffect 的 create 函数。

function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
  // At this point layout effects have already been destroyed (during mutation phase).
  // This is done to prevent sibling component effects from interfering with each other,
  // e.g. a destroy function in one component should never override a ref set
  // by a create function in another component during the same commit.
  if (shouldProfile(finishedWork)) {
    try {
      startLayoutEffectTimer();
      // 同步调用 useLayoutEffect 的 create 函数。
      commitHookEffectListMount(hookFlags, finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    recordLayoutEffectDuration(finishedWork);
  } else {
    try {
      // 同步调用 useLayoutEffect 的 create 函数。
      commitHookEffectListMount(hookFlags, finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
  }
}

自此 useLayoutEffect 挂载阶段执行完毕


useLayoutEffect update 更新阶段

第一:判断是函数节点的 tag 之后,调用 renderWithHooks.

/* 
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate 
*/
let value = renderWithHooks(_current,workInProgress,Component);

第二:在 renderWithHooks 当中调用 Counter 函数

let children = Component();

第三: 调用 Counter 函数的第一个 useEffect 函数

export function useEffect(create, deps) {
  return ReactCurrentDispatcher.current.useEffect(create, deps);
}

第四:更新阶段 ReactCurrentDispatcher.current.useEffect 实则是调用了 updateEffect, moutEffect 调用了 updateEffectImpl

return updateEffectImpl(
           UpdateEffect | PassiveEffect,
           HookPassive,
           create,
           deps
       );

第五:updateEffectImpl 函数: updateWorkInProgressHook 函数 复用 currentHook 的属性 创建新的 Hook 对象 以及 新的 Hook 链表, 调用 areHookInputsEqual 函数判断依赖是否发生变化,没有变化直接 return ,有变化调用 pushEffects

function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return;
      }
    }
  }
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

第六:等到 renderer 阶段中的 Mutation(Dom 挂载中阶段) 阶段,同步调用 layoutEffect 的 destroy 函数。

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
        // Layout effects are destroyed during the mutation phase so that all
        // destroy functions for all fibers are called before any create functions.
        // This prevents sibling component effects from interfering with each other,
        // e.g. a destroy function in one component should never override a ref set
        // by a create function in another component during the same commit.
        if (shouldProfile(finishedWork)) {
          try {
            startLayoutEffectTimer();
            // 同步调用 layoutEffect 的 destroy 函数
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
          recordLayoutEffectDuration(finishedWork);
        } else {
          try {
            // 同步调用 layoutEffect 的 destroy 函数
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
}

第七:等到 renderer 阶段中的 Layout (Dom 挂载后) 阶段,同步调用 useLayoutEffect 的 create 函数。

function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
  // At this point layout effects have already been destroyed (during mutation phase).
  // This is done to prevent sibling component effects from interfering with each other,
  // e.g. a destroy function in one component should never override a ref set
  // by a create function in another component during the same commit.
  if (shouldProfile(finishedWork)) {
    try {
      startLayoutEffectTimer();
      // 同步调用 useLayoutEffect 的 create 函数。
      commitHookEffectListMount(hookFlags, finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    recordLayoutEffectDuration(finishedWork);
  } else {
    try {
      // 同步调用 useLayoutEffect 的 create 函数。
      commitHookEffectListMount(hookFlags, finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
  }
}

自此 useLayoutEffect 更新阶段执行完毕