ZHANGYU.dev

October 14, 2023

useEffect和useLayoutEffect源码浅析

React15.4 min to read

上篇文章useState和useReducer源码浅析

以下源码浅析的React版本为17.0.1,使用ReactDOM.render创建的同步应用,不含优先级相关。

数据结构

Effect类型保存相关信息

type Effect = {|  tag: HookFlags, // 标识是useEffect还是useLayoutEffect(HasEffect、Layout、Passive )  create: () => (() => void) | void, // 回调函数  destroy: (() => void) | void, // 销毁回调函数  deps: Array<mixed> | null, // 依赖数组  next: Effect, // 下一个Effect|};

函数组件的Effect信息保存在函数组件Fiber节点的updateQueue字段,updateQueue为一个单向环形链表。

type FunctionComponentUpdateQueue = {|lastEffect: Effect | null|};

Fiber.updateQueue.lastEffect为最后一个EffectlastEffect.next为第一个Effect

这里值得注意的是Effect对象除了赋值给了updateQueue,同时也会赋值给Fiber节点中Hooks链表的对应HookmemoizedState属性,用于后续的对比。

useEffect和useLayoutEffect

之前的文章里讲到了Mount时和Update时用的Hook不是同一个,但是useEffectuseLayoutEffect在Mount和Update时用的都是同一个方法,只是传入了不同的参数。

mountEffect和mountLayoutEffect

// Mount时useEffectfunction mountEffect(  create: () => (() => void) | void,  deps: Array<mixed> | void | null,): void {  return mountEffectImpl(    UpdateEffect | PassiveEffect, // 赋值给Fiber.flags,与useLayoutEffect时不同的是多了个PassiveEffect    HookPassive, // 赋值给effect.tag    create, // 回调函数    deps, // 依赖数组  );}// Mount时的useLayoutEffectfunction mountLayoutEffect(  create: () => (() => void) | void,  deps: Array<mixed> | void | null,): void {  return mountEffectImpl(    UpdateEffect,     HookLayout, // 与useEffect不同    create,     deps,  );}

mountEffectmountLayoutEffect内部都是调用了mountEffectImpl,区别只是flags的标识不同,用于区分是useEffect还是useLayoutEffect

updateEffect和updateLayoutEffect

// Update时useEffectfunction updateEffect(  create: () => (() => void) | void,  deps: Array<mixed> | void | null,): void {  return updateEffectImpl(    UpdateEffect | PassiveEffect,    HookPassive,    create,    deps,  );}// Update时useLayoutEffectfunction updateLayoutEffect(  create: () => (() => void) | void,  deps: Array<mixed> | void | null,): void {  return updateEffectImpl(UpdateEffect, HookLayout, create, deps);}

和Mount时一样,Update时useEffectuseLayoutEffect内部使用的相同函数。

mountEffectImpl

mountEffectImpl函数里主要给Fiber节点添加了对应的flags,同时处理函数组件里Hooks链表,将Effect对象赋值给对应的workInProgressHookmemoizedState

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {  // 处理hooks链表同时返回workInProgressHook  const hook = mountWorkInProgressHook();  const nextDeps = deps === undefined ? null : deps;  // 为当前的Fiber节点添加flags  currentlyRenderingFiber.flags |= fiberFlags;  // pushEffect会返回Effect,同时赋值给workInProgressHook的memoizedState属性  hook.memoizedState = pushEffect(    HookHasEffect | hookFlags,    create,    undefined,    nextDeps,  );}

内部调用了pushEffect函数来创建Effect对象。

pushEffect

pushEffect函数创建了Effect对象,并组装updateQueue的单向环形链表。

function pushEffect(tag, create, destroy, deps) {  // effect对象  const effect: Effect = {    tag, // effect的tag,用于区分useEffect和useLayoutEffect    create, // 回调函数    destroy, // 销毁函数    deps, // 依赖数组    // 环形链表    next: (null: any),  };  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);  // fiber节点不存在updateQueue则需要初始化  if (componentUpdateQueue === null) {    // 创建新的updateQueue    componentUpdateQueue = createFunctionComponentUpdateQueue();    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);    // 初始值    componentUpdateQueue.lastEffect = effect.next = effect;  } else {    const lastEffect = componentUpdateQueue.lastEffect;    if (lastEffect === null) {      componentUpdateQueue.lastEffect = effect.next = effect;    } else {      // 单向环形链表 lastEffect为最新的effect,lastEffect.next为第一个effect      const firstEffect = lastEffect.next;      lastEffect.next = effect;      effect.next = firstEffect;      componentUpdateQueue.lastEffect = effect;    }  }  return effect;}

updateQueue不存在时,调用createFunctionComponentUpdateQueue创建新的updateQueue,否则就将新的effect添加到链表里。

createFunctionComponentUpdateQueue

createFunctionComponentUpdateQueue方法创建一个新的updateQueue

function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {  return {    lastEffect: null,  };}

updateEffectImpl

updateEffectImpl函数内部多了一个判断传入的依赖数组是否相等的判断。

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {  // 获取workInProgressHook,改变currentHook和workInProgressHook的指向  const hook = updateWorkInProgressHook();  const nextDeps = deps === undefined ? null : deps;  let destroy = undefined;  if (currentHook !== null) {    // 上一次的effect对象    const prevEffect = currentHook.memoizedState;    destroy = prevEffect.destroy;    if (nextDeps !== null) {      // 上一次的依赖数组      const prevDeps = prevEffect.deps;      // 判读两个依赖数组是否相同      if (areHookInputsEqual(nextDeps, prevDeps)) {        pushEffect(          hookFlags, // 对应的effect tag少了HookHasEffect,可视作无变化          create,           destroy,  // 和mount时不同传入了destroy          nextDeps,        );        // 直接return        return;      }    }  }  currentlyRenderingFiber.flags |= fiberFlags;  hook.memoizedState = pushEffect(    HookHasEffect | hookFlags,    create,    destroy,    nextDeps,  );}

到现在分析了useEffectuseLayoutEffect在Mount和Update时如何创建Effect对象,与useState不同的时,它们不能通过调用dispatchAction来主动触发更新,而是随着useState变化触发更新的同时随着Fiber树的构建在commit阶段执行回调函数和销毁函数。

但是useEffectuseLayoutEffect的回调函数和销毁函数执行的时机是不同的,这也是它们之间的直接区别。

useEffect的异步执行

workInProgressFiber树构建完成,进入commit阶段后,会异步调用useEffect的回调函数和销毁函数。

commit阶段内部又分为3个阶段

发起useEffect调度是在Before Mutation阶段执行的。

useEffect是异步调度的,需要执行回调函数和销毁函数的useEffect是在Layout阶段执行收集的,所以在最终异步处理useEffect的时候已经收集好了。

发起useEffect调度

Before Mutation阶段会执行commitBeforeMutationEffects函数,这个函数同时也会执行类组件的getSnapshotBeforeUpdate生命周期。

function commitBeforeMutationEffects() {  // 省略无关代码...  while (nextEffect !== null) {    const flags = nextEffect.flags;    // 当flags包含Passive时表示有调用useEffect    if ((flags & Passive) !== NoFlags) {      if (!rootDoesHavePassiveEffects) {        // 将全局标识赋值为true,一个异步调度就会处理所有的useEffect,避免发起多个        rootDoesHavePassiveEffects = true;        // 通过调度器发起一个异步调度        scheduleCallback(NormalSchedulerPriority, () => {          // 处理useEffect          flushPassiveEffects();          return null;        });      }    }    // 遍历有副作用的Fiber节点    nextEffect = nextEffect.nextEffect;  }}

flushPassiveEffects函数内部会调用flushPassiveEffectsImpl函数,在这里会执行回调函数和销毁函数,因为是异步调度的,已经是渲染结束后了。

收集需要处理的useEffect

上面说到需要执行回调函数和销毁函数的useEffect是在Layout阶段执行收集的。

Layout阶段会执行commitLayoutEffects函数,其中flags包含Update的Fiber节点的会执行commitLifeCycles函数。

function commitLifeCycles(  finishedRoot: FiberRoot,  current: Fiber | null,  finishedWork: Fiber,  committedLanes: Lanes,): void {  switch (finishedWork.tag) {    case FunctionComponent:    case ForwardRef:    case SimpleMemoComponent:    case Block: {      // 这里执行了useLayoutEffect的回调函数      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);      // 收集需要处理的useEffect      schedulePassiveEffects(finishedWork);      return;    }	// 省略无关代码...}

FunctionComponent会执行schedulePassiveEffects函数,schedulePassiveEffects函数中收集了需要执行回调函数和销毁函数的useEffect

function schedulePassiveEffects(finishedWork: Fiber) {  // updateQueue环形链表同时存了useEffect和useLayoutEffect的Effect对象  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;  if (lastEffect !== null) {    const firstEffect = lastEffect.next;    let effect = firstEffect;    // 遍历updateQueue    do {      const {next, tag} = effect;      if (        // HookPassive标识useEffect        (tag & HookPassive) !== NoHookEffect &&        // 当依赖数组没有发生变化时pushEffect的调用没有传入HookHasEffect,所以会被排除        (tag & HookHasEffect) !== NoHookEffect      ) {        // 需要执行销毁函数        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);        // 需要执行回调函数        enqueuePendingPassiveHookEffectMount(finishedWork, effect);      }      effect = next;    } while (effect !== firstEffect);  }}

需要执行销毁函数和回调函数的Effect对象分别存在两个数组中,数组的偶数下标为Effect对象,奇数下标为Fiber节点。

// 存需要执行回调函数let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];// 存需要执行销毁函数let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];

再看如何把Effect对象存入数组的。

// 需要执行回调函数export function enqueuePendingPassiveHookEffectMount(  fiber: Fiber,  effect: HookEffect,): void {  // 一次push两个不同数据,一个Effect对象,一个Fiber节点  pendingPassiveHookEffectsMount.push(effect, fiber);	// 省略代码...}// 需要执行销毁函数export function enqueuePendingPassiveHookEffectUnmount(  fiber: Fiber,  effect: HookEffect,): void {  pendingPassiveHookEffectsUnmount.push(effect, fiber);	// 省略代码...}

执行回调函数和销毁函数

这个过程由调度器异步调度执行,执行的函数为flushPassiveEffects的内部函数flushPassiveEffectsImpl

function flushPassiveEffectsImpl() {  // 省略代码...    const unmountEffects = pendingPassiveHookEffectsUnmount;  pendingPassiveHookEffectsUnmount = [];  // 执行销毁函数destroy函数  // 偶数下标为HookEffect,奇数下标为fiber节点  for (let i = 0; i < unmountEffects.length; i += 2) {    const effect = ((unmountEffects[i]: any): HookEffect);    const fiber = ((unmountEffects[i + 1]: any): Fiber);    const destroy = effect.destroy;    effect.destroy = undefined;    if (typeof destroy === 'function') {      try {        destroy();      } catch (error) {        captureCommitPhaseError(fiber, error);      }    }  }    // 执行回调函数create函数  const mountEffects = pendingPassiveHookEffectsMount;  pendingPassiveHookEffectsMount = [];  for (let i = 0; i < mountEffects.length; i += 2) {    const effect = ((mountEffects[i]: any): HookEffect);    const fiber = ((mountEffects[i + 1]: any): Fiber);    try {      const create = effect.create;      effect.destroy = create();    } catch (error) {      invariant(fiber !== null, 'Should be working on an effect.');      captureCommitPhaseError(fiber, error);    }  }    // 省略代码...}

useLayoutEffect的同步执行

useEffect不同,useLayoutEffect就完全是同步的了,并且不需要像useEffect一样去收集Effect对象,而是直接通过updateQueue执行。

useLayoutEffect的回调函数执行在Layout阶段,销毁函数执行在Mutation阶段。

执行回调函数

上面说到Layout阶段会执行commitLifeCycles函数。

function commitLifeCycles(  finishedRoot: FiberRoot,  current: Fiber | null,  finishedWork: Fiber,  committedLanes: Lanes,): void {  switch (finishedWork.tag) {    case FunctionComponent:    case ForwardRef:    case SimpleMemoComponent:    case Block: {      // 执行useLayoutEffect的回调函数      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);      // 收集需要处理的useEffect      schedulePassiveEffects(finishedWork);      return;    }	// 省略无关代码...}

commitLifeCycles函数里调用了commitHookEffectListMount函数执行useLayoutEffect的回调。

commitHookEffectListMount

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {  // 取出Fiber节点的updateQueue  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;  if (lastEffect !== null) {    const firstEffect = lastEffect.next;    let effect = firstEffect;    // 遍历执行回调函数create函数    do {      // (tag => HookLayout | HookHasEffect) 标识effect对象为useLayoutEffect      if ((effect.tag & tag) === tag) {        // 执行回调函数        const create = effect.create;        effect.destroy = create();      }      effect = effect.next;    } while (effect !== firstEffect);  }}

执行销毁函数

销毁函数的执行在Mutation阶段,Mutation阶段会执行commitMutationEffects函数,函数内部会对flags包含Update的Fiber节点再执行commitWork函数。

function commitWork(current: Fiber | null, finishedWork: Fiber): void {  switch (finishedWork.tag) {    case FunctionComponent:    case ForwardRef:    case MemoComponent:    case SimpleMemoComponent:    case Block: {      // 执行useLayoutEffect的销毁函数      commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);      return;    }  }}

commitHookEffectListUnmount

commitHookEffectListUnmount函数和commitHookEffectListMount函数逻辑那还就是一样。

function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {  // 取出Fiber节点的updateQueue  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 & tag) === tag) {        // 执行销毁函数        const destroy = effect.destroy;        effect.destroy = undefined;        if (destroy !== undefined) {          destroy();        }      }      effect = effect.next;    } while (effect !== firstEffect);  }}

至此useLayoutEffect执行完毕。

useImperativeHandle

useImperativeHandle相当于是一个useLayoutEffect的语法糖。

Mount

function mountImperativeHandle<T>(  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,  create: () => T,  deps: Array<mixed> | void | null,): void {  const effectDeps =    deps !== null && deps !== undefined ? deps.concat([ref]) : null;  return mountEffectImpl(    UpdateEffect,    HookLayout,    imperativeHandleEffect.bind(null, create, ref),    effectDeps,  );}

Update

function updateImperativeHandle<T>(  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,  create: () => T,  deps: Array<mixed> | void | null,): void {  const effectDeps =    deps !== null && deps !== undefined ? deps.concat([ref]) : null;  return updateEffectImpl(    UpdateEffect,    HookLayout,    imperativeHandleEffect.bind(null, create, ref),    effectDeps,  );}

内部使用的依然是mountEffectImpl方法和updateEffectImpl方法,唯一不同的是create函数传入的是经过处理的imperativeHandleEffect

imperativeHandleEffect

imperativeHandleEffect方法即是一个create方法,同时返回destroy函数。

function imperativeHandleEffect<T>(  create: () => T,  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,) {  if (typeof ref === 'function') {    const refCallback = ref;    const inst = create();    refCallback(inst);    return () => {      refCallback(null);    };  } else if (ref !== null && ref !== undefined) {    const refObject = ref;    const inst = create();    refObject.current = inst;    return () => {      refObject.current = null;    };  }}

函数内部会通过判断ref为对象还是回调函数,分别执行不同的逻辑,返回不同的destroy函数。知道了内部的原理,其实可以很容易的用useLayoutEffect复现。

function useFakeImperativeHandle(ref, create, deps) {  useLayoutEffect(() => {    const inst = create();    if (typeof ref === "function") {      ref(inst);      return () => {        ref(null);      };    } else if (ref) {      ref.current = inst;      return () => {        ref.current = null;      };    }  }, deps);}

总结

useEffectuseLayoutEffect的函数本身在Mount和Update时调用的都是相同的函数,仅参数不同,最大的区别在于useEffect是异步执行,useLayoutEffect是同步执行。

useEffectuseLayoutEffect所使用的Effect对象储存在函数组件的Fiber节点的updateQueue中,它是一个单向环形链表,updateQueue.lastEffect为最新的Effect对象,lastEffect.next为第一个Effect对象,同时为了维护函数组件的Hooks链表,Effect对象也同时被添加到了Fiber节点的memorizedState属性中。

Effect对象通过tag字段区分是useEffect还是useLayoutEffectHookPassiveuseEffectHookLayoutuseLayoutEffectHookHasEffect标记Effect的回调和销毁函数需要执行。

在Fiber树的render阶段通过renderWithHooKS方法执行函数组件,同时会执行内部的Hook,函数组件执行完成后创建了储存Effect对象的updateQueue链表。

在commit阶段,useEffect会在Before Mutation阶段通过commitBeforeMutationEffects函数发起异步调度,在Layout阶段通过函数commitLayoutEffects将需要执行回调函数和销毁函数的Effect分别收集到pendingPassiveHookEffectsMountpendingPassiveHookEffectsUnmount数组。在commit阶段完毕后会经过调度器执行回调函数和销毁函数。

useLayoutEffect是同步执行的,它的销毁函数在Mutation阶段通过commitMutationEffects函数最终调用commitHookEffectListUnmount函数执行。它的回调函数会在 Layout阶段通过commitLayoutEffects函数最终调用commitHookEffectListMount函数执行。

如有错误,还望交流指正。