React19源码系列之Hooks (useEffect、useLayoutEffect、useInsertionEffect)

上篇文章记录了useState钩子函数的源码,这篇文章记录 useEffect、useLayoutEffect 和 useInsertionEffect 钩子函数,主要看看它们的源码实现、区别和应用场景。

useEffect:useEffect – React 中文文档

useLayoutEffect:useLayoutEffect – React 中文文档

useInsertionEffect:useInsertionEffect – React 中文文档

useEffect

useEffect 是 React 中用于处理 副作用 的核心 Hook,主要用于执行与渲染无关的操作。

function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

mountEffect

mountEffect 函数是 React 中用于在组件挂载阶段执行副作用操作的钩子函数。在 React 里,副作用操作可以是数据获取、订阅事件、手动修改 DOM 等。mountEffect 会在组件首次渲染到 DOM 之后执行副作用函数,并且可以选择性地指定依赖项数组,用于控制副作用函数的重新执行。

函数参数含义:

  • create:这是一个必需的参数,是一个函数。该函数会在组件挂载后执行,它可以返回一个可选的清理函数(类型为 () => void),用于在组件卸载时执行清理操作,比如取消订阅、清除定时器等。如果不需要清理操作,create 函数可以不返回任何值(即返回 void)。
  • deps:这是一个可选参数,类型可以是数组(Array<mixed>)、void 或者 nulldeps 数组中的元素是副作用函数依赖的值。如果 deps 数组为空,副作用函数只会在组件挂载和卸载时执行;如果 deps 数组中的某个值发生变化,副作用函数会在组件重新渲染后再次执行。
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  
  mountEffectImpl(
    // 这是一个标志位,用于表示副作用的类型。
    // PassiveEffect 通常表示副作用是被动执行的,即不会阻塞渲染过程;
    //PassiveStaticEffect 可能与静态副作用相关。
    PassiveEffect | PassiveStaticEffect,
    
    HookPassive,// HookPassive:也是一个标志,用于标识这是一个被动的钩子类型
    create,// 回调函数
    deps,// 依赖项数组
  );
}

mountEffectImpl 函数

mountEffectImpl 函数是 React 内部用于实现副作用钩子(如 useEffect 在挂载阶段)的核心逻辑。它主要负责初始化钩子状态、设置 Fiber 节点的标志位,并将副作用信息存储在钩子的 memoizedState 中。

函数参数说明:

  • fiberFlags:类型为 Flags,是一个标志位,用于标识当前 Fiber 节点上副作用的类型和状态。不同的标志位组合可以表示不同的副作用行为,例如是否是被动副作用、是否需要立即执行等。
  • hookFlags:类型为 HookFlags,用于标识当前钩子的类型和状态,与 fiberFlags 类似,但更侧重于钩子本身的特性。
  • create:是一个函数,在组件挂载后会执行这个函数来产生副作用。它可以返回一个可选的清理函数(类型为 () => void),用于在组件卸载或副作用重新执行之前清理之前的副作用。
  • deps:依赖项数组,类型为 Array<mixed> | void | null。它决定了副作用函数在什么情况下会重新执行。如果 depsundefinednull,副作用函数会在每次渲染时都执行;如果 deps 是一个空数组,副作用函数只会在组件挂载和卸载时执行;如果 deps 数组中有值,当这些值发生变化时,副作用函数会重新执行。
function mountEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {

  // 创建并返回当前hook
  const hook = mountWorkInProgressHook();
  
  // 如果 deps 为 undefined,将 nextDeps 赋值为 null;否则,将 deps 赋值给 nextDeps
  const nextDeps = deps === undefined ? null : deps;

  // currentlyRenderingFiber 表示当前正在渲染的 Fiber 节点。通过按位或操作将 fiberFlags 合并到 currentlyRenderingFiber.flags 中,标记当前 Fiber 节点上存在特定类型的副作用。
  currentlyRenderingFiber.flags |= fiberFlags;

  // pushEffect:是 React 内部的一个函数,用于将副作用信息存储到一个链表中。
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,//标志位
    create,// 副作用函数
    createEffectInstance(),//创建一个副作用实例对象,该对象可能包含副作用的相关信息,如清理函数等。
    nextDeps,// 依赖项数字
  );
}

pushEffect 函数

pushEffect 函数是 React 内部用于将副作用信息添加到当前正在渲染的 Fiber 节点的更新队列中的核心函数。在 React 的函数式组件中,副作用(如 useEffect 产生的副作用)会被组织成一个循环链表,pushEffect 函数负责创建副作用对象并将其插入到这个链表中。

函数参数说明

  • tag:类型为 HookFlags,是一个标志位,用于标识副作用的类型和状态,例如是否需要执行、是否是被动副作用等。
  • create:是一个函数,用于创建副作用。这个函数在副作用执行时会被调用,并且它可以返回一个可选的清理函数,用于在副作用重新执行或组件卸载时清理之前的副作用。
  • inst:类型为 EffectInstance,是一个副作用实例对象,可能包含一些与副作用相关的元数据。
  • deps:依赖项数组,类型为 Array<mixed> | null。它决定了副作用在什么情况下会重新执行。如果 depsnull,副作用会在每次渲染时都执行;如果 deps 是一个数组,当数组中的值发生变化时,副作用会重新执行。
function pushEffect(
  tag: HookFlags,
  create: () => (() => void) | void,
  inst: EffectInstance,
  deps: Array<mixed> | null,
): Effect {
  
   // 创建一个 Effect 类型的对象 effect,包含了副作用的各种信息,如标志位 tag、创建函数 create、副作用实例 inst、依赖项数组 deps,以及一个指向下一个副作用对象的指针 next,初始化为 null
  const effect: Effect = {
    tag,
    create,
    inst,
    deps,
    // Circular
    next: (null: any),
  };
  
  // currentlyRenderingFiber 表示当前正在渲染的 Fiber 节点。
  //首先尝试从 currentlyRenderingFiber 的 updateQueue 属性中获取更新队列 componentUpdateQueue。
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);

  // 若更新队列为 null,则调用 createFunctionComponentUpdateQueue 函数创建一个新的更新队列,并将其赋值给 currentlyRenderingFiber 的 updateQueue 属性。
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
  }
  
  const lastEffect = componentUpdateQueue.lastEffect;
  // 如果 lastEffect 为 null,说明更新队列中还没有副作用对象,此时将 effect 的 next 指针指向自身,形成一个只有一个节点的循环链表,并将 componentUpdateQueue 的 lastEffect 指向 effect。
  if (lastEffect === null) {
    componentUpdateQueue.lastEffect = effect.next = effect;
    
  } else {
   // 构建循环队列
    const firstEffect = lastEffect.next;
    lastEffect.next = effect;
    effect.next = firstEffect;
    componentUpdateQueue.lastEffect = effect;
  }
  
  // 返回创建并插入到链表中的副作用对象。
  return effect;
}

createFunctionComponentUpdateQueue

React 中 函数组件更新队列 的创建逻辑,主要根据特性开关 enableUseMemoCacheHook 决定是否包含 memoCache 字段。

let createFunctionComponentUpdateQueue: () => FunctionComponentUpdateQueue;
// 一个布尔型特性开关,控制是否启用 useMemo 和 useCallback 的缓存优化。
if (enableUseMemoCacheHook) {
  createFunctionComponentUpdateQueue = () => {
    // 启用缓存优化的结构
    return {
      lastEffect: null, // 最后一个副作用链表
      events: null, // 事件处理相关状态
      stores: null, // 事件处理相关状态
      memoCache: null,  // memo 缓存(新增字段)
    };
  };
} else {
  createFunctionComponentUpdateQueue = () => {
    // 传统结构
    return {
      lastEffect: null,
      events: null,
      stores: null,
    };
  };
}

updateEffect

React 中 useEffect Hook 的 更新阶段 实现,主要用于 注册副作用 并在后续渲染时决定是否重新执行。

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  // PassiveEffect:表示这是一个异步副作用(在浏览器绘制后执行)。
  // HookPassive:标记 Hook 类型为 useEffect。
  // create:副作用创建函数。
  updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

updateEffectImpl

React 中 useEffectuseLayoutEffect核心实现,主要负责 注册、更新和比较副作用

function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
   // 获取当前 Hook 并准备依赖数组
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const effect: Effect = hook.memoizedState;
  // 存储副作用的实例信息,可能包含上一次的 cleanup 函数。
  const inst = effect.inst;

  // currentHook is null on initial mount when rerendering after a render phase
  // state update or for strict mode.
  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevEffect: Effect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
      
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 依赖未变化,仅添加 hookFlags(不包含 HookHasEffect)
        //hookFlags, 此时副作用仅被存储,不会在提交阶段执行。
        hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
        return;
      }
    }
  }

   // 标记 Fiber 并创建副作用
  // 对于 useEffect:PassiveEffect(异步执行)。
  // 对于 useLayoutEffect:LayoutEffect(同步执行)。
  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    // HookHasEffect:标记该副作用需要在提交阶段执行。
    HookHasEffect | hookFlags,
    create,
    inst,
    nextDeps,
  );
}

useLayoutEffect

function useLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useLayoutEffect(create, deps);
}

mountLayoutEffect

React 中 useLayoutEffect Hook 的 挂载阶段 实现,主要用于 注册同步执行的副作用

function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  // UpdateEffect:表示该 Fiber 需要执行副作用。
  // LayoutStaticEffect:表示这是一个布局副作用,需要同步执行。
  let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;

  return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}

updateLayoutEffect

React 中 useLayoutEffect Hook 的 更新阶段 实现,主要用于 比较依赖项 并决定是否需要更新或执行副作用。

function updateLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  // UpdateEffect:表示该 Fiber 需要执行副作用。
  // HookLayout:标记为 useLayoutEffect 类型的副作用。
  // create:副作用创建函数。
  return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

 useInsertionEffect

useInsertionEffect 是 React 18 新增的 Hook,用于在 DOM 插入后、布局计算前执行副作用。它主要用于 CSS-in-JS 库注入动态样式,以避免 布局抖动(Layout Shift)

function useInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useInsertionEffect(create, deps);
}

mountInsertionEffect

React 中 useInsertionEffect Hook 的 挂载阶段 实现,主要用于 在 DOM 插入后、布局计算前执行副作用

function mountInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  // UpdateEffect:表示该 Fiber 需要执行副作用。
  // HookInsertion:标记为插入阶段的副作用(useInsertionEffect)。
  mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}

updateInsertionEffect

React 中 useInsertionEffect Hook 的 更新阶段 实现,主要用于 在 DOM 插入后、布局计算前执行副作用,并在依赖项变化时重新执行。

function updateInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}

全局常量之 Hook 副作用类型 的位掩码常量

React 中 Hook 副作用类型 的位掩码常量,用于标记副作用的 执行时机状态。 

export type HookFlags = number;

export const NoFlags = /*   */ 0b0000;

// Represents whether effect should fire.
// 标记副作用需要执行。
export const HasEffect = /* */ 0b0001;

// Represents the phase in which the effect (not the clean-up) fires.
// 插入副作用(首次挂载时执行)。
export const Insertion = /* */ 0b0010;

// 布局副作用(同步执行)。
export const Layout = /*    */ 0b0100;

// 异步副作用(浏览器绘制后执行)。
export const Passive = /*   */ 0b1000;

useEffect 与 useLayoutEffect 的执行时机

useEffect 与 useLayoutEffect 的应用场景及说明

useEffect 与 useLayoutEffect 在组件中执行顺序

1、在组件 首次渲染 时:

  1. DOM 更新:React 更新 DOM 节点。
  2. useLayoutEffect:同步执行,可访问最新 DOM。
  3. 浏览器绘制:浏览器根据更新后的 DOM 绘制页面。
  4. useEffect:异步执行。

2、在组件 更新 时:

  1. DOM 更新:React 更新 DOM 节点。
  2. 上一次 useLayoutEffect 的 cleanup:同步执行。
  3. 本次 useLayoutEffect:同步执行。
  4. 浏览器绘制:浏览器绘制页面。
  5. 上一次 useEffect 的 cleanup:异步执行。
  6. 本次 useEffect:异步执行。

useEffect、useLayoutEffect、useInsertionEffect三者 的区别

关于查看mountWorkInProgressHook和updateWorkInProgressHook之前文章查看

React19源码系列之Hooks(useId)_react useid-优快云博客  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值