React源码系列之React启动过程分析

1,547 阅读4分钟

React源码系列之React启动过程分析

经历一个月的学习整理,站在前人的肩膀上,对 React 有了一些浅薄的理解,希望记录自己的学习过程的同时也可以给大家带来一点小帮助。如果此系列文章对您有些帮助,还望在座各位义夫义母不吝点赞关注支持 🐶,也希望各位大佬拍砖探讨

React 包概览

  • react react 基础包,只提供定义 react 组件(React Element)的必要函数,一般来说需要和渲染器(react-dom,react-native)一同使用.在编写 react 应用的代码时,大部分都是调此包的 api.

  • react-dom react 渲染器之一,是 react 与 web 平台连接的桥梁(可以在浏览和 nodejs 环境中使用),将 react-reconciler 中的运行结果输出到 web 界面上.在编写 react 应用的代码时,大多数场景下,能用到此包的就是一个入口函数 ReactDOM.render(<APP/>,doucument.getElementByID('root)'),其余使用的 api,基本是 react 包提供的

  • react- reconciler react 得以运行的核心包(综合协调 react-dom,react,scheduler 各包之前的调用与配合).管理 react 应用状态的输入和结果的输出.将输入信号最终转换成输出信号传递给渲染器

  • scheduler 调度机制的核心实现, 控制由 react-reconciler 送入的回调函数的执行时机, 在 concurrent 模式下可以实现任务分片. 在编写 react 应用的代码时, 同样几乎不会直接用到此包提供的 api. 核心任务就是执行回调(回调函数由 react-reconciler 提供) 通过控制回调函数的执行时机, 来达到任务分片的目的, 实现可中断渲染(concurrent 模式下才有此特性)

react 应用的启动过程

位于 react-dom 包,衔接 reconciler 运行流程中的输入步骤

启动模式

  1. legacy 模式: ReactDOM.render(<App />, rootNode). 这是当前 React app 使用的方式. 这个模式可能不支持这些新功能(concurrent 支持的所有功能).
// LegacyRoot
ReactDOM.render(<App />, document.getElementById('root'), dom => {}); // 支持callback回调, 参数是一个dom对象
  1. Blocking 模式: ReactDOM.createBlockingRoot(rootNode).render(<App />). 它仅提供了 concurrent 模式的小部分功能.
// BlockingRoot
// 1. 创建ReactDOMRoot对象
const reactDOMBlockingRoot = ReactDOM.createBlockingRoot(
  document.getElementById('root'),
);
// 2. 调用render
reactDOMBlockingRoot.render(<App />); // 不支持回调
  1. Concurrent 模式: ReactDOM.createRoot(rootNode).render(<App />). 实现了时间切片等功能
// ConcurrentRoot
// 1. 创建ReactDOMRoot对象
const reactDOMRoot = ReactDOM.createRoot(
  document.getElementById('root')
  );
// 2. 调用render
reactDOMRoot.render(<App />); // 不支持回调

启动流程

在调用入口函数之前,reactElement(和 DOM 对象 div#root之间没有关联

创建全局对象

三种模式下在 react 初始化时,都会创建 3 个全局对象

  1. ReactDOM(Blocking)Root 对象
    1. 属于 react-dom 包,该对象上有 render 和 unmount 方法,通过调用 render 方法可以引导 react 应用启动
  2. fiberRoot 对象
    1. 属于 react-reconciler 包, 作为 react-reconciler 在运行过程中的全局上下文, 保存 fiber 构建过程中所依赖的全局状态.
    2. react 利用这些信息来循环构造 fiber 节点(后续会详细分析 fiber 的构造过程)
  3. HostRootFiber 对象
    1. 属于 react-reconciler 包, 这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点, 节点的类型是 HostRoot.

创建 ReactDOM(Blocking)Root 对象

先放结论:

  1. 三种模式都会调用 updateContainer()函数,正是这个 updateContainer()函数串联了 react-dom 包和 react-reconciler.因为 updateContainer()函数中调用了 `scheduleUpdateOnFiber(xxx),进入 react 循环构造的唯一入口
  2. 三种模式都会创建在创建 DOMroot 中调用 createRootImpl,区分三种模式的方式只是传递的 rootTag 参数不同

看到这儿是不是觉得源码好像也就这么回事(神气)

legacy 模式


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是固定的, 并不是外界传入的
}
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. 创建 ReactDOM(Blocking)实例
  2. 调用 updateConatiner 进行更新

Concurrent 模式和 Blocking 模式

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

  1. 分别调用 ReactDOM.createRoot()ReactDOM.createBlockingRoot() 创建 ReactDOMRootReactDOMBlockingRoot 实例
  2. 调用 ReactDOMRootReactDOMBlockingRoot 实例的 render 方法
//BlockingRoot下调用createRoot
export function createRoot(
  container: Container,
  options?: RootOptions,
): RootType {
  return new ReactDOMRoot(container, options);
}

//Concurrent下调用ReactDOMBlockingRoot
export function createBlockingRoot(
  container: Container,
  options?: RootOptions,
): RootType {
  return new ReactDOMBlockingRoot(container, BlockingRoot, options); // 注意第2个参数BlockingRoot是固定写死的
}

继续查看ReactDOMRootReactDOMBlockingRoot

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);
}
//第一个共性:调用createRootImpl创建fiberRoot对象,并将其挂载到this._internalRoot上

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);
  });
};
//第二个共性:原型上有个render和unmount方法,且内部都会调用updateContainer进行更新

创建 fiberRoot 对象

无论哪种模式下, 在 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对象关联起来
  // TDOO:怎么关联起来的?
  //div#root._reactRootContainer = ReactDOM(lockingRoot)._internalRoot = FiberRoot.containerInfo->div#root
  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的传递
}
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;
}
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 树构建过程

进入更新入口

以上是对legacyRenderSubtreeIntoContainer中创建的追踪,结束后会回到legacyRenderSubtreeIntoContainer中进入更新容器的函数 unbatchedUpdate(...)

  1. legacy 回到 legacyRenderSubtreeIntoContainer 函数中有:
// 2. 更新容器
unbatchedUpdates(() => {
  updateContainer(children, fiberRoot, parentComponent, callback);
});

2.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);
};

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

不同点:

  1. legacy 下的更新会先调用 unbatchedUpdates, 更改执行上下文为 LegacyUnbatchedContext, 之后调用 updateContainer 进行更新.
  2. concurrentblocking 不会更改执行上下文, 直接调用 updateContainer 进行更新.
  3. fiber 循环构造的时候会根据执行上下文去对应不同的处理

继续追踪 updateContanier

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 };//对应的dom节点
  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. 需要关注其最后调用了 scheduleUpdateOnFiber循环构造的唯一入口

关于优先级,和 updateQueue 在此处做简要的理解,React 会根据当前时间戳去计算当前任务的优先级,并创建 update 对象挂载到 updateQueue 上(环形链表).之后 react 运行的过程中会按任务的优先级先后执行.之后会在优先级和 react 相关调度详细分析

更多系列文章会首发同名 VX 公众号

ReferenceList:

  1. github.com/7kms/react-…
  2. react.iamkasong.com/preparation…
  3. juejin.cn/post/708514…

本文由mdnice多平台发布