Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 源码学习笔记 #43

Open
jtwang7 opened this issue Mar 15, 2022 · 0 comments
Open

React 源码学习笔记 #43

jtwang7 opened this issue Mar 15, 2022 · 0 comments
Labels

Comments

@jtwang7
Copy link
Owner

jtwang7 commented Mar 15, 2022

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()

  • performUnitOfWork()

    • beginWork()

      • reconcileChildren()

        • mountChildFibers()

        • reconcileChildFibers()

    • completeWork()

commit阶段

commitRoot(fiberRootNode)

  • before mutation
    • commitBeforeMutationEffects()
  • mutation
    • commitMutationEffects()
      • Placement effect
        • commitPlacement()
      • Update effect
        • FunctionComponent mutation & commitHookEffectListUnmount()
        • HostComponent mutation & commitUpdate()
      • Deletion effect
        • commitDeletion()
  • layout
    • commitLayoutEffects()

内容

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阶段,分割出了两大类生命周期函数:

  • componentWillUnmount会在mutation阶段执行。此时current Fiber树还指向前一次更新的Fiber树,在生命周期钩子内获取的DOM还是更新前的。

  • componentDidMountcomponentDidUpdate会在layout阶段执行。此时current Fiber树已经指向更新后的Fiber树,在生命周期钩子内获取的DOM就是更新后的。

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对不同类型的节点分别处理。

  • 对于ClassComponent,他会通过current === null?区分是mount还是update,调用componentDidMountcomponentDidUpdate
  • 对于FunctionComponent及相关类型,他会调用useLayoutEffect hook回调函数调度useEffect销毁回调函数
  • 对于HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。
// 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~");
});
@jtwang7 jtwang7 added the React label Mar 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant