React源码阅读(4)-支线(函数式组件)-构建细节以及更新(下)

207 阅读6分钟

开篇

上一节中我们讲到了两个标准的State Hook(状态Hook),而这一节我们就去详细分析一下Effect Hook(副作用hook)

Effect Hook

前面我们已经对状态hook有了一定的了解,而对于effect hook最标准的就是useEffectuseLayoutEffect

1.useEffect和useLayoutEffect

我们先简单分析useEffectuseLayoutEffect的入参,他们基本是相同的唯一的不同仅有hook标示符HookPassiveHookLayout,最后都是调用mountEffectImpl

create:传入useEffect函数,即回调函数。

deps:依赖项,用来控制该Effect包裹的函数执不执行。如果依赖项为空数组[],则该Effect在每次组件挂载时执行,且仅执行一次,没有第二个参数则一直执行。

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

// Represents the phase in which the effect (not the clean-up) fires.
export const Layout = /*    */ 0b010;
export const Passive = /*   */ 0b100;

关于创建hook我们前面有提到,状态hook.memoizedState绑定的是状态,而副作用hook.memoizedState绑定的则是一个effect对象。这里去确立fiberflag是为了后续渲染fiber树使用的。

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 创建hook,确定在fiber.memoizedState链表中的位置
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 确定fiber的flag
  currentlyRenderingFiber.flags |= fiberFlags;
  // 挂载在hook.memoizedState上,state hook中挂载的是state状态,而effect hook中挂载的是effect链表
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

在这个effect hook中,我们以一个hook的视角讲一下指向,hook.memoizedState指向effct,effct是一个环形链表,他同时被挂载在fiber.updateQueue.lastEffecthook.memoizedState上。这里的逻辑也很简单就是处理创建effct,指向effct到链尾,返回effct

function pushEffect(tag, create, destroy, deps) {
  //  创建effect节点
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    next: (null: any),
  };
  // 把effect节点指向链尾
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    // 新建 currentlyRenderingFiber.updateQueue = workInProgress.updateQueue 用于挂载effect对象
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    // updateQueue.lastEffect是一个环形链表
    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;
    }
  }
  // 返回effect
  return effect;
}

紧接着我们就应该是去处理Effect的回调了,而Effect的回调是在commitRootImpl中的提交阶段去执行的,我们回顾一下一下提交阶段的三个子阶段:dom更新前commitBeforeMutationEffectsdom更新 commitMutationEffectsdom变更后 commitLayoutEffectsfiber的提交我们可以回顾一下前文,我们这里只关注对hook的处理。

1.1.dom更新之前

提交子阶段1中有提到,在这我们以一个effct hook视角来观察这个过程,在这里我们只关心一件事情,就是在变更之前将flushPassiveEffects交给了调度中心,去异步调用,而我们的整个commit在17.2中是同步的,而flushPassiveEffects是处理useEffect的,我们的后面几个阶段会为flushPassiveEffects处理useEffect先做一些准备工作,当准备工作做完后,我们再来分析flushPassiveEffects

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    if ((flags & Passive) !== NoFlags) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        scheduleCallback(NormalSchedulerPriority, () => {
          flushPassiveEffects();
          return null;
        });
      }
    }
  }
}

1.2.dom变更

在前文中我们提到了,如果需要更新节点,会执行commitWork,运算后useEffect,useLayoutEffect的值为Update,进去Update将内存中的fiber用于提交操作

function commitMutationEffects(
  root: FiberRoot,
  renderPriorityLevel: ReactPriorityLevel,
) {
  while (nextEffect !== null) {
    const flags = nextEffect.flags;
    // 运算后实际useEffect,useLayoutEffect的值为Update
    const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    switch (primaryFlags) {
      case Update: {
        // 将内存的中的fiber指向当前,进行更新节点操作
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

commitWorkdom变更阶段对于effect hook会处理销毁处理上一次的effectLayout来保证这一次阶段dom变更之后effectLayout的执行。

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
    switch (finishedWork.tag) {
        case FunctionComponent:
        case ForwardRef:
        case MemoComponent:
        case SimpleMemoComponent:
        case Block: {
          if (
            enableProfilerTimer &&
            enableProfilerCommitHooks &&
            finishedWork.mode & ProfileMode
          ) {
            try {
              startLayoutEffectTimer();
              // 销毁effectLayout函数
              commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
            } finally {
              recordLayoutEffectDuration(finishedWork);
            }
          } else {
            commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
          }
          return;
        }
        case ClassComponent: {
          return;
        }
 }
function commitHookEffectListUnmount(tag: number, 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 {
      // 这里只销毁effectLayout,对比的时候effct.tag如果为0不变更即不执行
      if ((effect.tag & tag) === tag) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

1.3 变更后

commitLayoutEffects这里我们在前面的构建章已经讲过,我们回顾一下,在这里的schedulePassiveEffects,为我们flushPassiveEffects提供了能够脱离fiber节点直接去访问effects的条件,因为它为我们保存了两个全局销毁挂载useEffect的数组。。

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
      ) {
        // 把effect添加到全局数组, 等待flushPassiveEffectsImpl处理
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }
      effect = next;
    } while (effect !== firstEffect);
  }
}

此时我们的准备工作已经做好了,来到处理useEffect的阶段。

export function flushPassiveEffects(): boolean {
  // Returns whether passive effects were flushed.
  if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
    const priorityLevel =
      pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
        ? NormalSchedulerPriority
        : pendingPassiveEffectsRenderPriority;
    pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
    // `runWithPriority`设置Schedule中的调度优先级, 如果在flushPassiveEffectsImpl中处理effect时又发起了新的更新, 那么新的update.lane将会受到这个priorityLevel影响.
    return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
  }
  return false;
}
function flushPassiveEffectsImpl() {
  if (rootWithPendingPassiveEffects === null) {
    return false;
  }
  rootWithPendingPassiveEffects = null;
  pendingPassiveEffectsLanes = NoLanes;

  // 执行所有的useEffect销毁函数,调用useEffect在上一次的渲染的销毁函数
  const unmountEffects = pendingPassiveHookEffectsUnmount;
  pendingPassiveHookEffectsUnmount = [];
  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') {
      destroy();
    }
  }

  // 执行所有的useEffect回调函数, 调用useEffect这次render的回调函数,重新赋值到 effect.destroy
  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);
    effect.destroy = create();
  }
}

到这我们分析mount Effct的过程已经告于段落。

1.4 更新effect

这里的逻辑和前面挂载effect几乎一样,多了最主要的内容是依赖比较(浅比较),如果依赖相同传hookFlags保证顺序,如果不同传HookHasEffect | hookFlagsHookHasEffect表示commit阶段需要重新执行。

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    // 继续使用先前effect.destroy
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}
// 浅比较
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (prevDeps === null) {
    return false;
  }
  
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  
  return true;
}

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

然后剩下的就和前面逻辑保持一致,commit会判断effect.tag决定是否执行。

ref hook

这里ref hook确实比较简单,相信如果有能力读懂上面写的,几乎ref是一看就懂的

export function useRef<T>(initialValue: T): {|current: T|} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}
function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();//获取useRef
  const ref = {current: initialValue};//ref初始化
  hook.memoizedState = ref;
  return ref;
}
// update时调用updateRef获取获取当前useRef,然后返回hook链表
function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

构建fiber阶段探寻,回溯处理

//beginWork中
function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    workInProgress.effectTag |= Ref;
  }
}
//completeWork中
function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}

提交阶段二,子阶段二commitMutationEffectsdom变更,如果ref改变了,先删除.

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;
    // ...
    
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);//移除ref
      }
    }
  }

提交阶段二,子阶段三commitLayoutEffect,重新设置ref

function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  // 忽略代码
  while (nextEffect !== null) {
   
    const flags = nextEffect.flags;
    // 处理标记
    if (flags & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }
    
    if (flags & Ref) {
      // 重新设置ref
      commitAttachRef(nextEffect);
    }

    // 忽略代码
    nextEffect = nextEffect.nextEffect;
  }

  // 忽略代码
}

useMemo和useCallback

mount挂载

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();//创建hook
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();//计算value
  hook.memoizedState = [nextValue, nextDeps];//把value和依赖保存在memoizedState中
  return nextValue;
}

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();//创建hook
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];//把callback和依赖保存在memoizedState中
  return callback;
}

update更新

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();//获取hook
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {//浅比较依赖
        return prevState[0];//没变 返回之前的状态
      }
    }
  }
  const nextValue = nextCreate();//有变化重新调用callback
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();//获取hook
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {//浅比较依赖
        return prevState[0];//没变 返回之前的状态
      }
    }
  }

  hook.memoizedState = [callback, nextDeps];//变了重新将[callback, nextDeps]赋值给memoizedState
  return callback;
}

总结

hook这一块这几天,感觉还是不够深入,这段时间先打打断点,深入分析一下,调用时机,算法,再重新修一下这整个大章节。