useEffect

effect也就是在React中我们常说的side effect,在React中类似像componentDidMount这样的生命周期方法中,因为可能会执行setState这样的方法而产生新的更新,我们称之为side effect即副作用。本身FunctionalComponent因为是pure function,所以不会产生任何的副作用,而useEffectuseLayoutEffect则是带给FunctionalComponent产生副作用能力的Hooks,他们的行为非常类似componentDidMountcomponentDidUpdate

他们接受一个方法作为参数,该方法会在每次渲染完成之后被调用;其次还接受第二个参数,是一个数组,这个数组里的每一个内容都会被用来进行渲染前后的对比,如果没有变化,则不会调用该副作用。

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

function pushEffect(tag, create, destroy, inputs) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    inputs,
    // Circular
    next: (null: any),
  };
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    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;
}

不难发现这个过程其实就是往当前Fiber上增加一系列effectTag,并且会创建updateQueue,这跟HostComponent类似,这个queue会在commit阶段被执行。这里我们需要注意的是useLayoutEffectuseEffect增加的effectTag是不一样的,所以他们执行的时机也是不一样的。effectTag会有以下几种情况:

  • useLayoutEffect增加UpdateEffect
  • useEffect增加UpdateEffect | PassiveEffect

以上是增加在Fiber对象上的,而记录对应Hook对象的effectTag如下:

  • useLayoutEffect增加UnmountMutation | MountLayout
  • useEffect增加UnmountPassive | MountPassive
  • 如果areHookInputsEqual符合,则增加NoHookEffect

记住这些内容,我们去看commit阶段做了什么跟Hook有关的内容

export function useLayoutEffect(
  create: () => mixed,
  inputs: Array<mixed> | void | null,
): void {
  useEffectImpl(UpdateEffect, UnmountMutation | MountLayout, create, inputs);
}

export function useEffect(
  create: () => mixed,
  inputs: Array<mixed> | void | null,
): void {
  useEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    inputs,
  );
}

function useEffectImpl(fiberEffectTag, hookEffectTag, create, inputs): void {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
  workInProgressHook = createWorkInProgressHook();

  let nextInputs = inputs !== undefined && inputs !== null ? inputs : [create];
  let destroy = null;
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (areHookInputsEqual(nextInputs, prevEffect.inputs)) {
      pushEffect(NoHookEffect, create, destroy, nextInputs);
      return;
    }
  }

  currentlyRenderingFiber.effectTag |= fiberEffectTag;
  workInProgressHook.memoizedState = pushEffect(
    hookEffectTag,
    create,
    destroy,
    nextInputs,
  );
}

commit阶段Hook相关的内容

在以下三个阶段都会调用commitHookEffectList方法,我们来看一下:

  • commitWorkcommitHookEffectList(UnmountMutation, MountMutation, finishedWork);
  • commitBeforeMutationLifeCyclescommitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
  • commitLifeCyclescommitHookEffectList(UnmountLayout, MountLayout, finishedWork);

commitHookEffectList这个方法的内容就是根据传入的unmountTagmountTag来判断是否需要执行对应的destorycreate方法,这是在每个Hook对象的effect链上的。所以看这部分代码最重要的其实就是看他传入的effectTag和Hook对象上的effectTag的对比。

对比结果就是:

  1. useLayoutEffectdestory会在commitWork的时候被执行;而他的create会在commitLifeCycles的时候被执行。
  2. useEffect在这个流程中都不会被执行。

可以看出来useLayoutEffect的执行过程跟componentDidMountcomponentDidUpdate非常相似,所以React官方也说了,如果你一定要选择一个类似于生命周期方法的Hook,那么useLayoutEffect是不会错的那个,但是我们推荐你使用useEffect,在你清除他们的区别的前提下,后者是更好的选择。

那么useEffect什么时候被调用呢?

答案在commitRoot的最后,他等其他sideEffect全部commit完了之后,会执行以下代码:

if (
  enableHooks &&
  firstEffect !== null &&
  rootWithPendingPassiveEffects !== null
) {
  let callback = commitPassiveEffects.bind(null, root, firstEffect);
  passiveEffectCallbackHandle = Schedule_scheduleCallback(callback);
  passiveEffectCallback = callback;
}

rootWithPendingPassiveEffects是在commitAllLifeCycles的时候如果发现更新中有passive effect的节点的话,就等于FiberRoot

if (enableHooks && effectTag & Passive) {
  rootWithPendingPassiveEffects = finishedRoot;
}

这里如果有,则会发起一次Schedule_scheduleCallback,这个就是我们之前讲的异步调度模块Scheduler的方法,流程跟PerformWork类似,这里我们不再重复讲解。

但我们看到这里就清楚了,useEffectdestorycreate都是异步调用的,所以他们不会影响本次更新的提交,所以不会因为在effect中产生了新的更新而导致阻塞DOM渲染的情况。

那么commitPassiveEffects做了啥呢?

export function commitPassiveHookEffects(finishedWork: Fiber): void {
  commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
  commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
}

正好对应了useEffect设置的sideEffect

function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,
) {
  if (!enableHooks) {
    return;
  }
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = null;
        if (destroy !== null) {
          destroy();
        }
      }
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;
        let destroy = create();
        if (typeof destroy !== 'function') {
          destroy = null;
        }
        effect.destroy = destroy;
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

results matching ""

    No results matching ""

    Jokcy的二维码

    扫码添加Jokcy,更多更新更优质的前端学习内容不断更新中,期待与你一起成长!

    Jokcy的二维码