Skip to content

useEffect

useEffect 是什么?

该 Hook 接收一个 包含命令式、且可能有副作用代码 的函数。

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。

栗子

  1. deps 为 [], 模拟的是类组件的 constructor 的功能,函数组件初次渲染的时候执行,更新的时候不执行
js
// 1. deps 为 [], 模拟的是类组件的 constructor 的功能
React.useEffect(() => {
  console.log("这是一个只有函数初始化才会执行的 effect");
}, []);
  1. deps为 [num]; 模拟的是 Vue 的 watch 的功能(immediate:true,初次也会执行),在 num 改变的时候会重新执行
js
// 2. deps为 [num]; 模拟的是Vue的watch的功能(immediate:true,初次也会执行)
React.useEffect(() => {
  console.log("effect可以添加deps,在deps改变的时候也会再次执行");
}, [num]);
  1. 函数返回值为 function; 用于进行副作用的清除工作;执行时机为
  • 在 effect 的组件要被卸载之前;
  • deps 更新重新执行 create 之前;
js
// 2. `函数返回值为 function;`  用于进行副作用的清除工作;
//   执行时机为 1. 在effect的组件要被卸载之前; 2. deps更新重新执行create之前
React.useEffect(() => {
  const timer = setTimeout(() => {
    setNum(2);
  }, 1000);
  // useEffect 可以返回一个函数(destroy)
  // 此函数在组件 1. 组件卸载前、2. 在执行下一个 effect 之前,上一个 effect 就已被清除 的时候会执行
  return () => {
    clearTimeout(timer);
  };
}, []);

flags

Hooks 类型的副作用其 flag 文件保存在 ReactHookEffectTags.js 文件中,其内容如下:

js
export type HookFlags = number;

export const NoFlags = /*  */ 0b000;

// Represents whether effect should fire.
// Hook 是否存在副作用
export const HasEffect = /* */ 0b001;

// Represents the phase in which the effect (not the clean-up) fires.
// useLayoutEffect 类型的副作用
export const Layout = /*    */ 0b010;
// useEffect       类型的副作用
export const Passive = /*   */ 0b100;

那么对于 useLayoutEffect 和 useEffect 这两个 Hook 其 tag 分别是什么?

  • useEffect

tag : 0b101转换为 10 进制为 5

原因: 因为其 1. 存在副作用0b001 2. 有 Passive 类型的副作用0b100 ; 所以合并起来就是 0b101

结果: 就存在这样的判断 (tag & HookPassive) !== NoHookEffect && (tag & HookHasEffect) !== NoHookEffect ) 去是否是 useEffect 类型的副作用

  • useLayoutEffect

    tag : 0b011转换为 10 进制为 3

    原因: 因为其 1. 存在副作用0b001 2. 有 Layout 类型的副作用0b100 ; 所以合并起来就是 0b011

    结果: 就存在这样的判断 (tag & HookLayout) !== NoHookEffect && (tag & HookHasEffect) !== NoHookEffect ) 去是否是 useLayoutEffect 类型的副作用

useEffect 的 保存

跟 useState 一样,useEffect 的执行也分为两种,一种在组件初次渲染的时候;一种在组件更新渲染的时候;

其核心流程都很相似,主要分为下面步骤:

  1. 创建或者获取 hook

  2. 在 FiberNode 上标记当前组件存在副作用。 FiberNode.flags |= fiberFlags ;(HookPassive)

  3. hook.memoizedState = pushEffect()

    1. 创建 effect 对象
    2. 在 FiberNode.updateQueue.lastEffect 创建或者在首位插入当前 effect
    3. 将 effect 作为 memoizedState 保存在 effect 的 hook 上

其结果如下

imageimage

重点:

  1. effect 类型的副作用 hook 的 memoizedState 跟 useState 渲染的数据类型不同,其不是一个 intitalValue 或者一个 func,而是一个effect 的对象
  2. effect 一方面保存到 hook 链上;也保存到 updateQueue 副作用链上;
  • hook 链上用来在更新阶段通过 updateWorkInProgressHook()去获取对应的 hook 实例对象
  • FiberNode.updateQueue 副作用链上 是用来表明这是一个 Update 副作用,需要在处理任务的时候进行副作用的处理

‍ 具体如下:

mountEffect

初次渲染的流程。可见就是构建 useEffect 的 hook 链updateQueue 的副作用链

js
/**
 * useEffect 和 useLayoutEffect 初次渲染的实现方法
 * @param {*} fiberFlags
 * @param {*} hookFlags
 * @param {*} create
 * @param {*} deps
 */
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 构建 hook
  const hook = mountWorkInProgressHook();
  // 创建 deps 订阅依赖
  const nextDeps = deps === undefined ? null : deps;
  // 在FiberNode上标记存在 HookPassive 的 effect
  currentlyRenderingFiber.flags |= fiberFlags;
  // 创建 effect , 并将 effect 添加到 1. FiberNode.updateQueue 2. hook.memoizedState
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps
  );
}

其中有两个重点入参:fiberFlagshookFlags 分别是什么作用?

fiberFlags

标记 Hook 存在 Update 类型的副作用,会标记在组件 FiberNode.flags 上 并在组件 commit 的去处理副作用从而生成新的 Update 任务

hookFlags

标志 Hook 副作用的执行时机, 如 Layout、Passive、Inserion 等

updateEffect

跟 mountEffect 的区别就是在 hook 作用链上 如果可以复用原来的 hook 就直接复用不需要重新生成

js
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 找到对应的 hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  // 在 updateWorkInProgressHook 会通过全局变量currentHook去指向当前的 hook
  if (currentHook !== null) {
    // 获取旧的effect对象
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    // 如果依赖存在
    if (nextDeps !== null) {
      // 旧的依赖
      const prevDeps = prevEffect.deps;
      // 判断依赖是否相等
      // 1. deps 一般为数组 需要下标和内容完全相等 [1,2,3] !== [1,3,2]
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 再次构建 FiberNode.updateQueue
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  // 在FiberNode上标记存在 HookPassive 的 effect
  currentlyRenderingFiber.flags |= fiberFlags;
  // 创建 effect , 并将 effect 添加到 1. FiberNode.updateQueue 2. hook.memoizedState
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps
  );
}

pushEffect

生成 effect 的对象,并将 effect 添加到 FiberNode.updateQueue 中

js
/**
 * 生成effect的对象,并将effect添加到 FiberNode.updateQueue中
 * @param {*} tag
 * @param {*} create
 * @param {*} destroy
 * @param {*} deps
 * @returns
 */
function pushEffect(tag, create, destroy, deps) {
  // 构建effect的对象
  const effect: Effect = {
    // effect的类型。就两种
    // useEffect -- 5
    // useLayoutEffect -- 5
    tag,
    // effect的执行函数
    create,
    // effect执行函数返回值 , 作为effect的销毁函数
    destroy,
    // 依赖
    deps,
    // Circular
    next: (null: any),
  };
  // 获取FiberNode.updateQueue
  let componentUpdateQueue: null | FunctionComponentUpdateQueue =
    (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    // updateQueue 那就初始化 并添加到 updateQueue.lastEffect 上
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // updateQueue不为空 , 生成一个以当前effect为第一位的 环状单项链表结构
    // updateQueue.lastEffect === currentEffect
    // currentEffect.next = oldlastEffect
    // oldlastEffect.next = currentEffect
    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;
}

执行时机

在函数组件 useEffect 执行的时候,其只是构建 effect 对象并将其保存在 hook 链 和 updateQueue 副作用链上,其具体怎么执行?怎么判断是否需要重新执行;什么执行 destroy?

这些都是在组件渲染的流程中进行的。具体可以看 Scheduler 任务调度的 commit 阶段的 flushPassiveEffects

flushPassiveEffects

js
/**
 * 执行当前副作用中的 useEffect 副作用
 *  1. 执行useEffect的销毁函数  useEffect(() => () => { // 销毁函数 })
 *  2. 执行useEffect的创建函数
 * @returns
 */
function flushPassiveEffectsImpl() {
  // 执行 需要被删除的旧节点的 , 存在Passive副作用的节点的 destroy 函数
  commitPassiveUnmountEffects(root.current);
  // 执行 需要被添加的新节点的, 存在Passive副作用的节点的 create 函数
  commitPassiveMountEffects(root, root.current, lanes, transitions);

  return true;
}

这里面涉及到两个参数

  • commitPassiveUnmountEffects 执行 需要被删除的旧节点的 , 存在 Passive 副作用的节点的 destroy 函数
  • commitPassiveMountEffects 执行 需要被添加的新节点的, 存在 Passive 副作用的节点的 create 函数

具体可以看 flushPassiveEffects

其通过两次深度优先遍历 去处理整个 FiberNode 中所有存在副作用的节点有关 useEffect 类型副作用。

  • 其中在 commitPassiveUnmountEffects 阶段对于存在 DeletionChild标记的节点,按照 先父后子 的顺序依次执行 函数式组件中存在的 useEffect 副作用的 destroy 函数

  • commitPassiveMountEffects阶段,再进行一次深度优先的遍历,去处理所有存在 PassiveMask标记的节点的 函数式组件中存在的 useEffect 副作用的 create 函数

结论

执行时机

  • useEffect的destroy函数 : 在 flushPassiveEffects 阶段执行,所以其在 DOM 更新之前
  • useEffect的create函数 : 在 flushPassiveEffects 阶段执行,所以其在 DOM 更新之前

执行顺序

  • useEffect的destroy函数 : 在 flushPassiveEffects 阶段执行,按照 先父后子的顺序执行
  • useEffect的create函数 : 在 flushPassiveEffects 阶段执行,按照 先子后父的顺序执行

关键信息

  1. fiberFlags
  • 初始化渲染阶段 : PassiveEffect | PassiveStaticEffect
  • 更新 渲染阶段 : PassiveEffect
  1. hookFlags
  • 初始化渲染阶段 : HookPassive
  • 更新 渲染阶段 : HookPassive