图解React图解React
  • 原理解析
  • 高频算法
  • 面试题
⌘ K
基本概念
宏观包结构
两大工作循环
高频对象
运行核心
启动过程
reconciler 运作流程
优先级管理
调度原理
fiber 树构造(基础准备)
fiber 树构造(初次创建)
fiber 树构造(对比更新)
fiber 树渲染
状态管理
状态与副作用
Hook 原理(概览)
Hook 原理(状态Hook)
Hook 原理(副作用Hook)
context 原理
交互
合成事件
Copyright © 2023 | Powered by dumi

React 应用的启动过程

在前文reconciler 运作流程把reconciler的流程归结成 4 个步骤.

本章节主要讲解react应用程序的启动过程, 位于react-dom包, 衔接reconciler 运作流程中的输入步骤.

在正式分析源码之前, 先了解一下react应用的启动模式:

在当前稳定版[email protected]源码中, 有 3 种启动方式. 先引出官网上对于这 3 种模式的介绍, 其基本说明如下:

  1. legacy 模式: ReactDOM.render(<App />, rootNode). 这是当前 React app 使用的方式. 这个模式可能不支持这些新功能(concurrent 支持的所有功能).

    // LegacyRoot
    ReactDOM.render(<App />, document.getElementById('root'), (dom) => {}); // 支持callback回调, 参数是一个dom对象
  2. Blocking 模式: ReactDOM.createBlockingRoot(rootNode).render(<App />). 目前正在实验中, 它仅提供了 concurrent 模式的小部分功能, 作为迁移到 concurrent 模式的第一个步骤.

    // BlockingRoot
    // 1. 创建ReactDOMRoot对象
    const reactDOMBlockingRoot = ReactDOM.createBlockingRoot(
    document.getElementById('root'),
    );
    // 2. 调用render
    reactDOMBlockingRoot.render(<App />); // 不支持回调
  3. Concurrent 模式: ReactDOM.createRoot(rootNode).render(<App />). 目前在实验中, 未来稳定之后,打算作为 React 的默认开发模式. 这个模式开启了所有的新功能.

    // ConcurrentRoot
    // 1. 创建ReactDOMRoot对象
    const reactDOMRoot = ReactDOM.createRoot(document.getElementById('root'));
    // 2. 调用render
    reactDOMRoot.render(<App />); // 不支持回调

注意: 虽然17.0.2的源码中有createRoot和createBlockingRoot方法(如果自行构建, 会默认构建experimental版本), 但是稳定版的构建入口排除掉了这两个 api, 所以实际在npm i react-dom安装17.0.2稳定版后, 不能使用该 api.如果要想体验非legacy模式, 需要显示安装 alpha 版本(或自行构建).

启动流程

在调用入口函数之前,reactElement(<App/>)和 DOM 对象div#root之间没有关联, 用图片表示如下:

创建全局对象 {#create-global-obj}

无论Legacy, Concurrent或Blocking模式, react 在初始化时, 都会创建 3 个全局对象

  1. ReactDOM(Blocking)Root对象
  • 属于react-dom包, 该对象暴露有render,unmount方法, 通过调用该实例的render方法, 可以引导 react 应用的启动.
  1. fiberRoot对象

    • 属于react-reconciler包, 作为react-reconciler在运行过程中的全局上下文, 保存 fiber 构建过程中所依赖的全局状态.
    • 其大部分实例变量用来存储fiber 构造循环(详见两大工作循环)过程的各种状态.react 应用内部, 可以根据这些实例变量的值, 控制执行逻辑.
  2. HostRootFiber对象

    • 属于react-reconciler包, 这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点, 节点的类型是HostRoot.

这 3 个对象是 react 体系得以运行的基本保障, 一经创建大多数场景不会再销毁(除非卸载整个应用root.unmount()).

这一过程是从react-dom包发起, 内部调用了react-reconciler包, 核心流程图如下(其中红色标注了 3 个对象的创建时机).

下面逐一解释这 3 个对象的创建过程.

创建 ReactDOM(Blocking)Root 对象

由于 3 种模式启动的 api 有所不同, 所以从源码上追踪, 也对应了 3 种方式. 最终都 new 一个ReactDOMRoot或ReactDOMBlockingRoot的实例, 需要创建过程中RootTag参数, 3 种模式各不相同. 该RootTag的类型决定了整个 react 应用是否支持可中断渲染(后文有解释).

下面根据 3 种 mode 下的启动函数逐一分析.

legacy 模式

legacy模式表面上是直接调用ReactDOM.render, 跟踪ReactDOM.render后续调用legacyRenderSubtreeIntoContainer(源码链接)

function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
) {
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// 初次调用, root还未初始化, 会进入此分支
//1. 创建ReactDOMRoot对象, 初始化react应用环境
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function () {
// instance最终指向 children(入参: 如<App/>)生成的dom节点
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// 2. 更新容器
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// root已经初始化, 二次调用render会进入
// 1. 获取FiberRoot对象
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function () {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// 2. 调用更新
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}

继续跟踪legacyCreateRootFromDOMContainer. 最后调用new ReactDOMBlockingRoot(container, LegacyRoot, options);

function legacyCreateRootFromDOMContainer(
container: Container,
forceHydrate: boolean,
): RootType {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
return createLegacyRoot(
container,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}
export function createLegacyRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMBlockingRoot(container, LegacyRoot, options); // 注意这里的LegacyRoot是固定的, 并不是外界传入的
}

通过以上分析,legacy模式下调用ReactDOM.render有 2 个核心步骤:

  1. 创建ReactDOMBlockingRoot实例(在 Concurrent 模式和 Blocking 模式中详细分析该类), 初始化 react 应用环境.
  2. 调用updateContainer进行更新.

Concurrent 模式和 Blocking 模式

Concurrent模式和Blocking模式从调用方式上直接可以看出

  1. 分别调用ReactDOM.createRoot和ReactDOM.createBlockingRoot创建ReactDOMRoot和ReactDOMBlockingRoot实例
  2. 调用ReactDOMRoot和ReactDOMBlockingRoot实例的render方法
export function createRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMRoot(container, options);
}
export function createBlockingRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMBlockingRoot(container, BlockingRoot, options); // 注意第2个参数BlockingRoot是固定写死的
}

继续查看ReactDOMRoot和ReactDOMBlockingRoot对象

function ReactDOMRoot(container: Container, options: void | RootOptions) {
// 创建一个fiberRoot对象, 并将其挂载到this._internalRoot之上
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
function ReactDOMBlockingRoot(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// 创建一个fiberRoot对象, 并将其挂载到this._internalRoot之上
this._internalRoot = createRootImpl(container, tag, options);
}
ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
// 执行更新
updateContainer(children, root, null, null);
};
ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount =
function (): void {
const root = this._internalRoot;
const container = root.containerInfo;
// 执行更新
updateContainer(null, root, null, () => {
unmarkContainerAsRoot(container);
});
};

ReactDOMRoot和ReactDOMBlockingRoot有相同的特性

  1. 调用createRootImpl创建fiberRoot对象, 并将其挂载到this._internalRoot上.
  2. 原型上有render和unmount方法, 且内部都会调用updateContainer进行更新.

创建 fiberRoot 对象 {#create-root-impl}

无论哪种模式下, 在ReactDOM(Blocking)Root的创建过程中, 都会调用一个相同的函数createRootImpl, 查看后续的函数调用, 最后会创建fiberRoot 对象(在这个过程中, 特别注意RootTag的传递过程):

// 注意: 3种模式下的tag是各不相同(分别是ConcurrentRoot,BlockingRoot,LegacyRoot).
this._internalRoot = createRootImpl(container, tag, options);
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// ... 省略部分源码(有关hydrate服务端渲染等, 暂时用不上)
// 1. 创建fiberRoot
const root = createContainer(container, tag, hydrate, hydrationCallbacks); // 注意RootTag的传递
// 2. 标记dom对象, 把dom和fiber对象关联起来
markContainerAsRoot(root.current, container);
// ...省略部分无关代码
return root;
}
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
// 创建fiberRoot对象
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); // 注意RootTag的传递
}

创建 HostRootFiber 对象

在createFiberRoot中, 创建了react应用的首个fiber对象, 称为HostRootFiber(fiber.tag = HostRoot)

export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 创建fiberRoot对象, 注意RootTag的传递
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// 1. 这里创建了`react`应用的首个`fiber`对象, 称为`HostRootFiber`
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 2. 初始化HostRootFiber的updateQueue
initializeUpdateQueue(uninitializedFiber);
return root;
}

在创建HostRootFiber时, 其中fiber.mode属性, 会与 3 种RootTag(ConcurrentRoot,BlockingRoot,LegacyRoot)关联起来.

export function createHostRootFiber(tag: RootTag): Fiber {
let mode;
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BlockingMode | StrictMode;
} else if (tag === BlockingRoot) {
mode = BlockingMode | StrictMode;
} else {
mode = NoMode;
}
return createFiber(HostRoot, null, null, mode); // 注意这里设置的mode属性是由RootTag决定的
}

注意:fiber树中所有节点的mode都会和HostRootFiber.mode一致(新建的 fiber 节点, 其 mode 来源于父节点),所以HostRootFiber.mode非常重要, 它决定了以后整个 fiber 树构建过程.

运行到这里, 3 个对象创建成功, react应用的初始化完毕.

将此刻内存中各个对象的引用情况表示出来:

  1. legacy

  1. concurrent

  1. blocking

注意:

  1. 3 种模式下,HostRootFiber.mode是不一致的
  2. legacy 下, div#root和ReactDOMBlockingRoot之间通过_reactRootContainer关联. 其他模式是没有关联的
  3. 此时reactElement(<App/>)还是独立在外的, 还没有和目前创建的 3 个全局对象关联起来

调用更新入口

  1. legacy
    回到legacyRenderSubtreeIntoContainer函数中有:
// 2. 更新容器
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
  1. concurrent 和 blocking
    在ReactDOM(Blocking)Root原型上有render方法
ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
// 执行更新
updateContainer(children, root, null, null);
};

相同点:

  1. 3 种模式在调用更新时都会执行updateContainer. updateContainer函数串联了react-dom与react-reconciler, 之后的逻辑进入了react-reconciler包.

不同点:

  1. legacy下的更新会先调用unbatchedUpdates, 更改执行上下文为LegacyUnbatchedContext, 之后调用updateContainer进行更新.

  2. concurrent和blocking不会更改执行上下文, 直接调用updateContainer进行更新.

继续跟踪updateContainer函数

export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
// 1. 获取当前时间戳, 计算本次更新的优先级
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
// 2. 设置fiber.updateQueue
const update = createUpdate(eventTime, lane);
update.payload = { element };
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
enqueueUpdate(current, update);
// 3. 进入reconciler运作流程中的`输入`环节
scheduleUpdateOnFiber(current, lane, eventTime);
return lane;
}

updateContainer函数位于react-reconciler包中, 它串联了react-dom与react-reconciler. 此处暂时不深入分析updateContainer函数的具体功能, 需要关注其最后调用了scheduleUpdateOnFiber.

在前文reconciler 运作流程中, 重点分析过scheduleUpdateOnFiber是输入阶段的入口函数.

所以到此为止, 通过调用react-dom包的api(如: ReactDOM.render), react内部经过一系列运转, 完成了初始化, 并且进入了reconciler 运作流程的第一个阶段.

思考

可中断渲染

react 中最广为人知的可中断渲染(render 可以中断, 部分生命周期函数有可能执行多次, UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps)只有在HostRootFiber.mode === ConcurrentRoot | BlockingRoot才会开启. 如果使用的是legacy, 即通过ReactDOM.render(<App/>, dom)这种方式启动时HostRootFiber.mode = NoMode, 这种情况下无论是首次 render 还是后续 update 都只会进入同步工作循环, reconciliation没有机会中断, 所以生命周期函数只会调用一次.

对于可中断渲染的宣传最早来自2017 年 Lin Clark 的演讲. 演讲中阐述了未来 react 会应用 fiber 架构, reconciliation可中断等(13:15 秒). 在v16.1.0中应用了 fiber.

在最新稳定版v17.0.2中, 可中断渲染虽然实现, 但是并没有在稳定版暴露出 api. 只能安装 alpha 版本才能体验该特性.

但是不少开发人员认为稳定版本的react已经是可中断渲染(其实是有误区的), 大概率也是受到了各类宣传文章的影响. 前端大环境还是比较浮躁的, 在当下, 更需要静下心来学习.

总结

本章节介绍了react应用的 3 种启动方式. 分析了启动后创建了 3 个关键对象, 并绘制了对象在内存中的引用关系. 启动过程最后调用updateContainer进入react-reconciler包,进而调用schedulerUpdateOnFiber函数, 与reconciler运作流程中的输入阶段相衔接.