• 【React 源码】(五)React 应用的启动过程


    React 应用的启动过程

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

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

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

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

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

      1. // LegacyRoot
      2. ReactDOM.render(<App />, document.getElementById('root'), dom => {}); // 支持callback回调, 参数是一个dom对象

    2. Blocking 模式ReactDOM.createBlockingRoot(rootNode).render(). 目前正在实验中, 它仅提供了 concurrent 模式的小部分功能, 作为迁移到 concurrent 模式的第一个步骤.

      1. // BlockingRoot
      2. // 1. 创建ReactDOMRoot对象
      3. const reactDOMBlockingRoot = ReactDOM.createBlockingRoot(
      4. document.getElementById('root'),
      5. );
      6. // 2. 调用render
      7. reactDOMBlockingRoot.render(<App />); // 不支持回调

    3. Concurrent 模式ReactDOM.createRoot(rootNode).render(). 目前在实验中, 未来稳定之后,打算作为 React 的默认开发模式. 这个模式开启了所有的新功能.

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

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

    启动流程

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

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

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

    1. ReactDOM(Blocking)Root对象
    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 一个ReactDOMRootReactDOMBlockingRoot的实例, 需要创建过程中RootTag参数, 3 种模式各不相同. 该RootTag的类型决定了整个 react 应用是否支持可中断渲染(后文有解释).

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

    legacy 模式

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

    1. function legacyRenderSubtreeIntoContainer(
    2. parentComponent: ?React$Component,
    3. children: ReactNodeList,
    4. container: Container,
    5. forceHydrate: boolean,
    6. callback: ?Function,
    7. ) {
    8. let root: RootType = (container._reactRootContainer: any);
    9. let fiberRoot;
    10. if (!root) {
    11. // 初次调用, root还未初始化, 会进入此分支
    12. //1. 创建ReactDOMRoot对象, 初始化react应用环境
    13. root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
    14. container,
    15. forceHydrate,
    16. );
    17. fiberRoot = root._internalRoot;
    18. if (typeof callback === 'function') {
    19. const originalCallback = callback;
    20. callback = function() {
    21. // instance最终指向 children(入参: 如)生成的dom节点
    22. const instance = getPublicRootInstance(fiberRoot);
    23. originalCallback.call(instance);
    24. };
    25. }
    26. // 2. 更新容器
    27. unbatchedUpdates(() => {
    28. updateContainer(children, fiberRoot, parentComponent, callback);
    29. });
    30. } else {
    31. // root已经初始化, 二次调用render会进入
    32. // 1. 获取FiberRoot对象
    33. fiberRoot = root._internalRoot;
    34. if (typeof callback === 'function') {
    35. const originalCallback = callback;
    36. callback = function() {
    37. const instance = getPublicRootInstance(fiberRoot);
    38. originalCallback.call(instance);
    39. };
    40. }
    41. // 2. 调用更新
    42. updateContainer(children, fiberRoot, parentComponent, callback);
    43. }
    44. return getPublicRootInstance(fiberRoot);
    45. }
    46. 继续跟踪legacyCreateRootFromDOMContainer. 最后调用new ReactDOMBlockingRoot(container, LegacyRoot, options);
    47. function legacyCreateRootFromDOMContainer(
    48. container: Container,
    49. forceHydrate: boolean,
    50. ): RootType {
    51. const shouldHydrate =
    52. forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
    53. return createLegacyRoot(
    54. container,
    55. shouldHydrate
    56. ? {
    57. hydrate: true,
    58. }
    59. : undefined,
    60. );
    61. }
    62. export function createLegacyRoot(
    63. container: Container,
    64. options?: RootOptions,
    65. ): RootType {
    66. return new ReactDOMBlockingRoot(container, LegacyRoot, options); // 注意这里的LegacyRoot是固定的, 并不是外界传入的
    67. }

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

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

    Concurrent 模式和 Blocking 模式

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

    1. 分别调用ReactDOM.createRootReactDOM.createBlockingRoot创建ReactDOMRootReactDOMBlockingRoot实例
    2. 调用ReactDOMRootReactDOMBlockingRoot实例的render方法
    1. export function createRoot(
    2. container: Container,
    3. options?: RootOptions,
    4. ): RootType {
    5. return new ReactDOMRoot(container, options);
    6. }
    7. export function createBlockingRoot(
    8. container: Container,
    9. options?: RootOptions,
    10. ): RootType {
    11. return new ReactDOMBlockingRoot(container, BlockingRoot, options); // 注意第2个参数BlockingRoot是固定写死的
    12. }
    13. 继续查看ReactDOMRootReactDOMBlockingRoot对象
    14. function ReactDOMRoot(container: Container, options: void | RootOptions) {
    15. // 创建一个fiberRoot对象, 并将其挂载到this._internalRoot之上
    16. this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
    17. }
    18. function ReactDOMBlockingRoot(
    19. container: Container,
    20. tag: RootTag,
    21. options: void | RootOptions,
    22. ) {
    23. // 创建一个fiberRoot对象, 并将其挂载到this._internalRoot之上
    24. this._internalRoot = createRootImpl(container, tag, options);
    25. }
    26. ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
    27. children: ReactNodeList,
    28. ): void {
    29. const root = this._internalRoot;
    30. // 执行更新
    31. updateContainer(children, root, null, null);
    32. };
    33. ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = function(): void {
    34. const root = this._internalRoot;
    35. const container = root.containerInfo;
    36. // 执行更新
    37. updateContainer(null, root, null, () => {
    38. unmarkContainerAsRoot(container);
    39. });
    40. };

    ReactDOMRootReactDOMBlockingRoot有相同的特性

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

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

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

    1. // 注意: 3种模式下的tag是各不相同(分别是ConcurrentRoot,BlockingRoot,LegacyRoot).
    2. this._internalRoot = createRootImpl(container, tag, options);
    3. function createRootImpl(
    4. container: Container,
    5. tag: RootTag,
    6. options: void | RootOptions,
    7. ) {
    8. // ... 省略部分源码(有关hydrate服务端渲染等, 暂时用不上)
    9. // 1. 创建fiberRoot
    10. const root = createContainer(container, tag, hydrate, hydrationCallbacks); // 注意RootTag的传递
    11. // 2. 标记dom对象, 把dom和fiber对象关联起来
    12. markContainerAsRoot(root.current, container);
    13. // ...省略部分无关代码
    14. return root;
    15. }
    16. export function createContainer(
    17. containerInfo: Container,
    18. tag: RootTag,
    19. hydrate: boolean,
    20. hydrationCallbacks: null | SuspenseHydrationCallbacks,
    21. ): OpaqueRoot {
    22. // 创建fiberRoot对象
    23. return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); // 注意RootTag的传递
    24. }
    25. 创建 HostRootFiber 对象
    26. 在createFiberRoot中, 创建了react应用的首个fiber对象, 称为HostRootFiber(fiber.tag = HostRoot)
    27. export function createFiberRoot(
    28. containerInfo: any,
    29. tag: RootTag,
    30. hydrate: boolean,
    31. hydrationCallbacks: null | SuspenseHydrationCallbacks,
    32. ): FiberRoot {
    33. // 创建fiberRoot对象, 注意RootTag的传递
    34. const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
    35. // 1. 这里创建了`react`应用的首个`fiber`对象, 称为`HostRootFiber`
    36. const uninitializedFiber = createHostRootFiber(tag);
    37. root.current = uninitializedFiber;
    38. uninitializedFiber.stateNode = root;
    39. // 2. 初始化HostRootFiber的updateQueue
    40. initializeUpdateQueue(uninitializedFiber);
    41. return root;
    42. }
    43. 在创建HostRootFiber时, 其中fiber.mode属性, 会与 3RootTag(ConcurrentRoot,BlockingRoot,LegacyRoot)关联起来.
    44. export function createHostRootFiber(tag: RootTag): Fiber {
    45. let mode;
    46. if (tag === ConcurrentRoot) {
    47. mode = ConcurrentMode | BlockingMode | StrictMode;
    48. } else if (tag === BlockingRoot) {
    49. mode = BlockingMode | StrictMode;
    50. } else {
    51. mode = NoMode;
    52. }
    53. return createFiber(HostRoot, null, null, mode); // 注意这里设置的mode属性是由RootTag决定的
    54. }

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

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

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

    1. legacy

    1. concurrent

    1. blocking

    注意:

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

    调用更新入口

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

    相同点:

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

    不同点:

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

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

    继续跟踪updateContainer函数

    1. export function updateContainer(
    2. element: ReactNodeList,
    3. container: OpaqueRoot,
    4. parentComponent: ?React$Component,
    5. callback: ?Function,
    6. ): Lane {
    7. const current = container.current;
    8. // 1. 获取当前时间戳, 计算本次更新的优先级
    9. const eventTime = requestEventTime();
    10. const lane = requestUpdateLane(current);
    11. // 2. 设置fiber.updateQueue
    12. const update = createUpdate(eventTime, lane);
    13. update.payload = { element };
    14. callback = callback === undefined ? null : callback;
    15. if (callback !== null) {
    16. update.callback = callback;
    17. }
    18. enqueueUpdate(current, update);
    19. // 3. 进入reconciler运作流程中的`输入`环节
    20. scheduleUpdateOnFiber(current, lane, eventTime);
    21. return lane;
    22. }

    updateContainer函数位于react-reconciler包中, 它串联了react-domreact-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(, 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运作流程中的输入阶段相衔接.

  • 相关阅读:
    Linux之进度条
    接口测试必备知识点(含实战项目)
    医疗大模型:数据+知识双轮驱动实现医学推理、医患问答、病历自动生成、临床决策,为未来医疗服务提供全新可能性
    【PCL】PCL点云在Qt中可视化
    torch.eq的广播机制兼谈快速生成对角掩码
    java版工程管理系统Spring Cloud+Spring Boot+Mybatis实现工程管理系统源码
    通过热透镜聚焦不同类型的高斯模式
    常见的数据结构及应用
    【PAT甲级 - C++题解】1149 Dangerous Goods Packaging
    Redis key基本使用
  • 原文地址:https://blog.csdn.net/weixin_44828588/article/details/126467620