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. hook的memoizedState跟useState不同,不是一个 intitalValue或者一个 func,而是一个effect的对象
  2. effect一方面保存到hook链上;也保存到 updateQueue副作用链上;

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

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 的执行

在函数组件useEffect执行的时候,其只是构建effect对象并将其保存在 hook链 和 updateQueue副作用链上,其具体怎么执行,怎么判断是否需要重新执行;什么执行 destroy? 等都是在组件渲染的流程中进行的。具体可以看Scheduler任务调度的commit阶段的 commitBeforeMutationEffects

对于渲染阶段其主要分为三个步骤

  1. commitBeforeMutationEffects​
  2. commitMutationEffects
  3. commitLayoutEffects

commitBeforeMutaition

image

flushPassiveEffects

js
/**
 * 执行当前副作用中的 useEffect 副作用
 *  1. 执行useEffect的销毁函数  useEffect(() => () => { // 销毁函数 })
 *  2. 执行useEffect的创建函数
 * @returns
 */
function flushPassiveEffectsImpl() {
  // 第一步: 处理销毁函数
  const unmountEffects = pendingPassiveHookEffectsUnmount;
  pendingPassiveHookEffectsUnmount = [];
  for (let i = 0; i < unmountEffects.length; i += 2) {
     // .... 执行 effect.desctroy
  }
  // Second pass: Create new passive effects.
  // 第二步: 执行useEffect 的 创建函数
  const mountEffects = pendingPassiveHookEffectsMount;
  pendingPassiveHookEffectsMount = [];
  for (let i = 0; i < mountEffects.length; i += 2) {
     // .... 执行 effect.create
  }
  return true;
}

这里面涉及到两个参数

  • pendingPassiveHookEffectsUnmount 保存了当前需要卸载的effect的列表
  • pendingPassiveHookEffectsMount 保存了当前需要create的effect的列表

commitMutationEffects

在commitMutation 会处理组件的 commitLifeCycles生命周期,如遇到函数组件会执行一个方法 ,

从而通过遍历updateQueue.lastEffect上的tag == 5 的副作用,加入到pendingPassiveHookEffectsUnmount 、pendingPassiveHookEffectsMount ,在下一个任务队列通过flushPassiveEffects 去 先执行 destroy 再执行 create

js
/**
 * 函数组件 在 commitMutationEffects 阶段执行
 *  收集当前 FiberNode上的 useEffect 类型的副作用,并将其加入到
 *  - pendingPassiveHookEffectsUnmount  destroy类型的
 *  - pendingPassiveHookEffectsMount    create 类型的
 * @param {*} finishedWork
 */
function schedulePassiveEffects(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 {
      const {next, tag} = effect;
      // 判断是 useEffect 类型的 effect
      if (
        (tag & HookPassive) !== NoHookEffect &&
        (tag & HookHasEffect) !== NoHookEffect
      ) {
        // 
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }
      effect = next;
    } while (effect !== firstEffect);
  }
}

useLayoutEffect

作用

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useLayoutEffect的保存

js
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
}

可见在生成阶段其就是 FiberNode.flags 和 effect.tag不同

useLayoutEffect 和 useEffect 的异同点

相同点

  1. 都是effect类型的hook

    • 都会在函数组件执行阶段在FiberNode.memoizedState 上生成一个 hook 对象
    • 都会在函数组件执行阶段在FiberNode.updateQueue 上添加一个 effect副作用
  2. 缓存数据结构、语法(deps、create、destroy)都很像

不同点

  1. 执行时机不同

    • useLayoutEffect

      • create在commit的最后阶段(commitLayoutEffects)中才会执行,并生成destroy
      • destroy在commit的第二阶段(commitMutationEffects)中如果是函数类型组件的卸载操作,就会执行destroy 回调
    • useEffect是在flushPassiveEffects 的时候统一执行(destroy -> create)

  2. 执行的方式不同

    • useLayoutEffect 他是在组件Commit阶段同步执行的
    • useLayout : 在组件Commit阶段收集,然后通过scheduler高优先级任务队列回调执行的