jtwang7 / React-Note

React 学习笔记
8 stars 2 forks source link

React 源码学习笔记 #43

Open jtwang7 opened 2 years ago

jtwang7 commented 2 years ago

React 技术揭秘学习笔记

参考文章:React技术揭秘

// start
performSyncWorkOnRoot() || performConcurrentWorkOnRoot() => fiberRootNode
// --- render ---

// --- commit ---
commitRoot(fiberRootNode)
- before mutation // 执行DOM操作前
  - commitBeforeMutationEffects()
- mutation // 执行DOM操作
  - commitMutationEffects()
    // 根据effectTag分别调用不同的方法处理
    - Placement effect
      - commitPlacement()
    - Update effect
      - FunctionComponent mutation & commitHookEffectListUnmount() // 遍历effectList,执行所有useLayoutEffect hook的销毁函数
      - HostComponent mutation & commitUpdate() // 将render阶段中为Fiber节点赋值的updateQueue对应内容渲染到页面
    - Deletion effect
      - commitDeletion()
- layout // 执行DOM操作后

导航

render阶段

performSyncWorkOnRoot() || performConcurrentWorkOnRoot()

commit阶段

commitRoot(fiberRootNode)

内容

before mutation

遍历effectList并调用commitBeforeMutationEffects函数处理。

// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);

// 将当前上下文标记为CommitContext,作为commit阶段的标志
const prevExecutionContext = executionContext;
executionContext |= CommitContext;

// 处理focus状态
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;

// beforeMutation阶段的主函数
commitBeforeMutationEffects(finishedWork);

focusedInstanceHandle = null;

commitBeforeMutationEffects()

整体可以分为三部分:

  1. 处理DOM节点渲染/删除后的 autoFocusblur 逻辑。
  2. 调用getSnapshotBeforeUpdate生命周期钩子。
  3. 调度useEffect。❗️注意:是调度useEffect而不是调用。

整个useEffect异步调用分为三步:

  1. before mutation阶段scheduleCallback中调度flushPassiveEffects

    flushPassiveEffects方法内部执行流程:

    从全局变量rootWithPendingPassiveEffects获取effectList(Placement|Update|Deletion|effectTag),然后遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。

  2. layout阶段之后将effectList赋值给rootWithPendingPassiveEffects

  3. scheduleCallback触发flushPassiveEffectsflushPassiveEffects内部遍历rootWithPendingPassiveEffects

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const current = nextEffect.alternate;

    if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
      // ...focus blur相关
    }

    const effectTag = nextEffect.effectTag;

    // 调用getSnapshotBeforeUpdate
    if ((effectTag & Snapshot) !== NoEffect) {
      commitBeforeMutationEffectOnFiber(current, nextEffect);
    }

    // 调度useEffect
    if ((effectTag & Passive) !== NoEffect) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        // scheduleCallback()由Scheduler模块提供:以某个优先级异步调度一个回调函数
        // 此处flushPassiveEffects()将以normal优先级被异步调用
        scheduleCallback(NormalSchedulerPriority, () => {
          // flush副作用队列:此处触发useEffect回调
          flushPassiveEffects(); 
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

关于getSnapshotBeforeUpdate()

生命周期钩子getSnapshotBeforeUpdate()实际是替代React v15中render阶段UNSAFE_componentWillxxx()生命周期函数的解决方案。

React v16开始,Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillxxx())可能触发多次。为了解决该问题,React v16 在 commit 的 before mutation 阶段内重新实现了类似功能的getSnapshotBeforeUpdate(),由于commit阶段是同步且无法中断的,因此避免了多次调用的情况。


mutation

遍历effectList并调用commitMutationEffects函数处理。

nextEffect = firstEffect;
// 循环遍历effectList
do {
  try {
      // 调用commitMutationEffects()
      commitMutationEffects(root, renderPriorityLevel);
    } catch (error) {
      invariant(nextEffect !== null, 'Should be working on an effect.');
      captureCommitPhaseError(nextEffect, error);
      nextEffect = nextEffect.nextEffect;
    }
} while (nextEffect !== null);

commitMutationEffects

commitMutationEffects会遍历effectList,对每个Fiber节点执行如下三个操作:

  1. 根据ContentReset effectTag重置文字节点
  2. 更新ref
  3. 根据effectTag分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating[服务端渲染相关])
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  // 遍历effectList
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;

    // 根据 ContentReset effectTag重置文字节点
    if (effectTag & ContentReset) {
      commitResetTextContent(nextEffect);
    }

    // 更新ref
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }

    // 根据 effectTag 分别处理
    const primaryEffectTag =
      effectTag & (Placement | Update | Deletion | Hydrating);
    switch (primaryEffectTag) {
      // 插入DOM
      case Placement: {
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        break;
      }
      // 插入DOM 并 更新DOM
      case PlacementAndUpdate: {
        // 插入
        commitPlacement(nextEffect);

        nextEffect.effectTag &= ~Placement;

        // 更新
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // SSR
      case Hydrating: {
        nextEffect.effectTag &= ~Hydrating;
        break;
      }
      // SSR
      case HydratingAndUpdate: {
        nextEffect.effectTag &= ~Hydrating;

        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 更新DOM
      case Update: {
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 删除DOM
      case Deletion: {
        commitDeletion(root, nextEffect, renderPriorityLevel);
        break;
      }
    }

    nextEffect = nextEffect.nextEffect;
  }
}

Placement effect & commitPlacement

Placement effectTag: 插入标记,意味着该Fiber节点对应的DOM节点需要插入到页面中。

调用commitPlacement方法执行Placement effectTag操作。

// 1. 获取父级DOM节点。其中finishedWork为传入的Fiber节点
const parentFiber = getHostParentFiber(finishedWork);
// 父级DOM节点
const parentStateNode = parentFiber.stateNode;

// 2. 获取Fiber节点的DOM兄弟节点
const before = getHostSibling(finishedWork);

// 3. 根据DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作
// parentStateNode是否是rootFiber
if (isContainer) {
 insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
  insertOrAppendPlacementNode(finishedWork, before, parent);
}

Update effect & commitWork

Update effectTag: 更新标记,意味着该Fiber节点需要更新并将结果映射为DOM节点插入到页面中。

调用commitWork方法执行Update effectTag操作。commitWork()会根据Fiber.tag分别处理不同的Fiber类型

主要关注FunctionComponentHostComponent

FunctionComponent mutation

fiber.tagFunctionComponent,会调用commitHookEffectListUnmount。该方法会遍历effectList,执行所有useLayoutEffect hook的销毁函数。

HostComponent mutation

fiber.tagHostComponent,会调用commitUpdate。最终会在updateDOMProperties (opens new window)中将render阶段 completeWork (opens new window)中为Fiber节点赋值的updateQueue对应的内容渲染在页面上。

Deletion effect & commitDeletion

Deletion effectTag: 删除标记,意味着该Fiber节点对应的DOM节点需要从页面中删除。

调用commitDeletion方法执行Deletion effectTag操作。

commitDeletion 方法会执行如下操作:

  1. 递归调用Fiber节点及其子孙Fiber节点fiber.tagClassComponentcomponentWillUnmount (opens new window)生命周期钩子,从页面移除Fiber节点对应DOM节点
  2. 解绑ref
  3. 调度useEffect的销毁函数

layout

遍历effectList,执行commitLayoutEffects()函数。

该阶段的代码都是在DOM渲染完成(mutation阶段完成)后执行的。该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM

root.current = finishedWork; // current Fiber树切换

nextEffect = firstEffect;
// 遍历effectList
do {
  try {
    // 执行 commitLayoutEffects()
    commitLayoutEffects(root, lanes);
  } catch (error) {
    invariant(nextEffect !== null, "Should be working on an effect.");
    captureCommitPhaseError(nextEffect, error);
    nextEffect = nextEffect.nextEffect;
  }
} while (nextEffect !== null);

nextEffect = null;

root.current = finishedWork

双缓存机制一节介绍过,workInProgress Fiber树commit阶段完成渲染后会变为current Fiber树

root.current = finishedWork的作用就是切换fiberRootNode指向的current Fiber树

也就是说,commit 阶段渲染完成后切换树的操作发生在mutation阶段结束后,layout阶段开始前,即 root.current = finishedWork这行代码

以该行代码为界,分隔成mutation阶段layout阶段,分割出了两大类生命周期函数:

commitLayoutEffects

commitLayoutEffects一共做了两件事:

  1. commitLayoutEffectOnFiber(调用生命周期钩子hook相关操作, 触发状态更新this.setState如果赋值了第二个参数回调函数,也会在此时调用。)
  2. commitAttachRef(赋值 ref)
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    // 调用生命周期钩子和hook
    if (effectTag & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }

    // 赋值ref
    if (effectTag & Ref) {
      commitAttachRef(nextEffect);
    }

    nextEffect = nextEffect.nextEffect;
  }
}

commitLayoutEffectOnFiber

  1. 调用生命周期钩子
  2. 调用hook相关操作
  3. 触发状态更新this.setState如果赋值了第二个参数回调函数,也会在此时调用。

commitLayoutEffectOnFiber方法会根据fiber.tag对不同类型的节点分别处理。

// FunctionComponent 相关操作
switch (finishedWork.tag) {
    // 以下都是FunctionComponent及相关类型
  case FunctionComponent:
  case ForwardRef:
  case SimpleMemoComponent:
  case Block: {
    // 执行useLayoutEffect的回调函数
    commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
    // 调度useEffect的销毁函数与回调函数
    schedulePassiveEffects(finishedWork);
    return;
  }

useLayoutEffectuseEffect的区别:

useLayoutEffect hookmutation阶段销毁函数调用到本次更新layout阶段回调函数调用是同步执行的。

useEffect则需要先调度,在Layout阶段完成后再异步执行。

// HostRoot(即rootFiber) 相关操作
ReactDOM.render(<App />, document.querySelector("#root"), function() {
  console.log("i am mount~");
});