zhangyu1818.

Tag »React

useEffect和useLayoutEffect源码浅析

2021年 2月24日在Github上查看

上篇文章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时useEffect
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect, // 赋值给Fiber.flags,与useLayoutEffect时不同的是多了个PassiveEffect
    HookPassive, // 赋值给effect.tag
    create, // 回调函数
    deps, // 依赖数组
  );
}

// Mount时的useLayoutEffect
function 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时useEffect
function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

// Update时useLayoutEffect
function 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个阶段

  • Before Mutation阶段
  • Mutation阶段
  • Layout阶段

发起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函数执行。

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