React源码流程

508 阅读6分钟

流程

入口

ReactDOM.render(<App />, rootNode) 同步

lane === SyncLane 这个条件是成立的,因此会直接进入 performSyncWorkOnRoot 的逻辑,开启同步的 render 流程;而在异步渲染模式下,则将进入 else 的逻辑。

ReactDOM.createRoot(rootNode).render(<App />) 异步

else 逻辑 还需要分

ensureRootIsScheduled 这个方法

该方法很关键,它将决定如何开启当前更新所对应的 render 阶段

ensureRootIsScheduled 中 核心逻辑

if (newCallbackPriority === SyncLanePriority) {
  // 同步更新的 render 入口

  newCallbackNode = scheduleSyncCallback(
    performSyncWorkOnRoot.bind(null, root)
  );
} else {
  // 将当前任务的 lane 优先级转换为 scheduler 可理解的优先级

  var schedulerPriorityLevel =
    lanePriorityToSchedulerPriority(newCallbackPriority);

  // 异步更新的 render 入口

  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
}

React 会以当前更新任务的优先级类型为依据,决定接下来是调度 performSyncWorkOnRoot 还是 performConcurrentWorkOnRoot

初始化

  • jsx的转换

  • FiberRoot 构建(OnlyOne)

    • ReactDOM.render()会产生多个rootFiber
    • fiberRoot 和 rootFiber 建立起关联

首次渲染时采用 ReactSyncRoot 进行同步渲染,不会进入异步调度过程,因为组件需要尽快的完成渲染。最终渲染完成后生成一颗完整的 fiber 树。

  • 完整的 FiberTree

Scheduler(调度器)

调度任务的优先级,高优任务优先进入 更新

时间切片原理

接下来我们开启 Concurrent Mode(开启后会启用时间切片)

// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(<App/>, rootEl);
ReactDOM.unstable_createRoot(rootEl).render(<App />);

时间切片的本质是模拟实现requestIdleCallback

requestIdleCallback例子

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script>
      const taskList = [
        {
          id: 1,
          msg: "first task",
        },
        {
          id: 2,
          msg: "second task",
        },
        {
          id: 3,
          msg: "third task",
        },
        {
          id: 4,
          msg: "four task",
        },
      ];

      function wait(time) {
        const now = Date.now();

        while (Date.now() - now < time) {
          // console.log('--->');
        }
      }

      function oldExecuteTask(list = taskList) {
        list.forEach((item) => {
          console.log("execute task", item.msg);
          wait(1000);
        });
      }

      function newExecuteTask(list = taskList) {
        requestIdleCallback(
          () => {
            const firstList = list[0];
            console.log(`execute task ${firstList.msg}`);
            wait(1000);

            list.length > 1 && newExecuteTask(list.slice(1));
          },
          { timeout: 2000 }
        );
      }
    </script>
  </head>

  <body>
    <button onclick="oldExecuteTask()">execute old task</button>
    <button onclick="newExecuteTask()">execute new task</button>
    <button onclick="(() => {console.log('other task')})()">other task</button>
  </body>
</html>
一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback

优先级调度

SchedulerReact 是两套优先级机制。在 React 中,存在多种使用不同优先级的情况,比如:

以下例子皆为 Concurrent Mode 开启情况

  • 过期任务或者同步任务使用同步优先级

  • 用户交互产生的更新(比如点击事件)使用高优先级

  • 网络请求产生的更新使用一般优先级

  • Suspense 使用低优先级

React 需要设计一套满足如下需要的优先级机制:

  • 可以表示优先级的不同

  • 可能同时存在几个同优先级的更新,所以还得能表示批的概念

  • 方便进行优先级相关计算

为了满足如上需求,React 设计了 lane 模型。接下来我们来看 lane 模型如何满足以上 3 个条件。

Fiber 更新过程(Fiber Reconciler(协调器))

render 阶段

new Fiber Tree生成

Fiber 节点是如何被创建并构建 Fiber 树的。 Fiber --> Fiber Tree

递阶段

首先从 rootFiber 开始向下深度优先遍历。为遍历到的每个 Fiber 节点调用 beginWork 方法。

该方法会根据传入的 Fiber 节点创建子 Fiber 节点,并将这两个 Fiber 节点连接起来。

当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段

React 中最多会同时存在两棵 Fiber

CurrentTreeWorkInProgressTree

在第一次渲染之后(mount),react 最终得到一个 fiber tree

react 开始处理更新时,它会构建一个 workInProgress tree,它反映了要刷新到屏幕的未来状态。(new Fiber Tree)

beginWork 的工作可以分为两部分

update 时:如果 current 存在,在满足一定条件时可以复用 current 节点,这样就能克隆 current.child 作为 workInProgress.child,而不需要新建 workInProgress.child。

mount 时:除 fiberRootNode 以外,current === null。会根据 fiber.tag 不同,创建不同类型的子 Fiber 节点

“归”阶段

在“归”阶段会调用 completeWork (opens new window)处理 Fiber 节点。

当某个 Fiber 节点执行完 completeWork,如果其存在兄弟 Fiber 节点(即 fiber.sibling !== null),会进入其兄弟 Fiber 的“递”阶段。

如果不存在兄弟 Fiber,会进入父级 Fiber 的“归”阶段。

“递”和“归”阶段会交错执行直到“归”到 rootFiber。至此,render 阶段的工作就结束了。

Effect

                  nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber

commit 阶段只需要遍历 effectList 就能执行所有 effect 了

借用 React 团队成员 Dan Abramov 的话:effectList 相较于 Fiber 树,就像圣诞树上挂的那一串彩灯。

可打断,React 在 workingProgressTree 上复用 current 上的 Fiber 数据结构来一步地(通过 requestIdleCallback)来构建新的 tree,标记处需要更新的节点,放入队列中。

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

他们唯一的区别是是否调用 shouldYield。如果当前浏览器帧没有剩余时间,shouldYield 会中止循环,直到浏览器有空闲时间后再继续遍历

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

双缓存

这种在内存中构建并直接替换的技术叫做双缓存

React 使用“双缓存”来完成 Fiber 树的构建与替换——对应着 DOM 树的创建与更新。

复用

workInProgress fiber的创建可以复用current Fiber树对应的节点数据。

workInProgress Fiber 树在 render 阶段完成构建后进入 commit 阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为 current Fiber 树

Renderer(渲染器)

commit阶段

commit 阶段的主要工作(即 Renderer 的工作流程)分为三部分

  • before mutation 阶段(执行 DOM 操作前)

  • mutation 阶段(执行 DOM 操作)

  • layout 阶段(执行 DOM 操作后)

commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参。

类似git commit 提交代码

commit 阶段会触发一些生命周期钩子(如 componentDidXXX)和 hook(如 useLayoutEffect、useEffect)。

Renderer 根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。

useLayouEffectuseEffect的区别

在这些回调方法中可能触发新的更新,新的更新会开启新的render-commit流程

更新粒度

树级更新

采用这种更新方式最有名的框架就是 React

React每次更新都会从rootFiber(根 Fiber 节点)向下深度优先遍历, 通过alternate 复用 再次重新生成一颗Fiber Tree

「树级更新」的框架会再生成一棵完整「虚拟 DOM 树」,生成过程中与之前的「虚拟 DOM 树」对应节点进行比较: 依赖「虚拟 DOM」

不关心触发更新的节点(因为会通过「虚拟 DOM」的全树对比找到他)

组件级更新

采用这种更新方式最有名的框架就是 Vue

会找到触发更新节点所在组件,生成该组件的「虚拟 DOM 树」(而不是全树的「虚拟 DOM 树」),生成过程中与该组件之前的「虚拟 DOM 树」对应节点进行比较

依赖「虚拟 DOM」关心触发更新的节点(「虚拟 DOM」的对比会作用于该节点所在组件)

节点级更新

采用这种更新方式最有名的框架就是 Svelte。

如果是「节点级更新」框架,在编译时会根据「状态变化对应的 DOM 变化」直接生成对应方法,当状态改变后直接调用对应方法。

不依赖「虚拟 DOM」,依赖预编译(建立状态与改变 DOM 的方法之间的联系)

关心触发更新的节点(节点状态与更新方法一一对应)

VUE3

Vue 作为「组件级更新」代表,更新粒度介于「树级」与「节点级」之间。那到底是中间偏左呢,还是中间偏右呢?

我要反复横跳,两边我都要

当使用 JSX 时,Vue3 拥有了 React 运行时的灵活性,此时的 Vue3 可以看作是「加强版 React + Mobx」

注意

concurrent,面向未来的开发模式。我们之前讲的任务中断/任务优先级都是针对 concurrent 模式

更新链路疏通同归

  • mount

首次同步生成的 Fiber Tree

  • update

参考

前端日志

React 技术揭秘

Build your own React

中国人中庸之道,中国人造的框架呢