小程序架构指南(四):源码详解小程序启动原理(上)

上回说到, 借助 React Fiber 架构提供的能力, 我们可以基于 React 完成小程序架构. 但由于篇幅所限, 我们只概要描述了下思路而略过了核心原理和实现方案. 在这篇文章中, 我们会以基于同样构建思路的remax@2.15.0为例, 分析类小程序项目中项目的具体启动过程.

通过之前的文章我们知道, 小程序的基本启动模型是:

  1. 解析 app.json, 获取其中注册的JSX对象和对应的 path
  2. 初始化实现了HostConfig协议所约定接口的对象, 作为负责实际渲染的容器Container
  3. 获取待渲染的JSX对象
    1. 从 Native 中获取当前打开的 scheme, 解析出正在访问的路径&参数
    2. 和已注册路由进行比较
      1. 如果匹配到已注册 path, 则加载对应的JSX对象
      2. 否则加载默认页面对应的JSX对象
        1. [可选]如果没找到匹配路径, 也可以直接报白屏错误, 看小程序引擎实现者的心情
  4. Container对象, 和JSX对象 一起传入由Reconciler导出的render方法
  5. 在传统浏览器环境中
    1. Reconciler会将JSX渲染为虚拟 Dom
    2. 期间根据JSX变动, 不断产生更新指令, 将指令转换为HostConfig中约定的 Dom 操作, 并调用Container暴露的操作方法.
    3. Container根据被调用的操作, 创建实际 Dom. 从而生成实际页面
  6. 在实际小程序运行环境中
    1. 由于小程序环境中逻辑层和渲染层分开展示, 因此在逻辑层中运行的Container并不会创建实际 Dom.
    2. 所以在小程序应用中, 我们引入一个中间层, 用 js 对象模拟 Dom 操作, 并记录Reconciler传入的 Dom 操作指令.
    3. 在一个操作批次结束后, 将操作指令 json 化, 变成字符串格式的指令列表
    4. 通过Native转发给位于渲染层的webview-render对象
    5. webview-render对象根据操作指令, 在 webview 中构建实际 Dom

也就是这个模型

ReactElement对象 -> Render(React-Reconciler) -> Container(HostConfig) -> 转发命令 -> Webview-Render

我们以Remax@2.15.0React@16.7.0为例, 结合实际代码对启动流程进行一次跟踪

小程序启动示例代码如下所示

1
2
3
4
5
6
7
8
// 最简小程序模型.
// https://github.dev/remaxjs/remax/blob/v2.15.0/packages/remax-runtime/src/__tests__/index.test.tsx#L53
import Container from "@remax/remax-runtime/Container";
import render from "@remax/remax-runtime/render";

const MiniProgramPage = () => <View className="foo">hello</View>;
const container = new Container();
render(<MiniProgramPage />, container);

在这段代码中, 我们完成了以下工作:

  1. 直接获取待渲染的 jsx 对象 MiniProgramPage
  2. 在逻辑层内初始化 Dom 容器 Container, 用于在 js-core 中模拟 Dom 功能, 接收并缓存后续ReactReconciler传过来的 Dom 指令
  3. jsx对象Container传给 render, 进入渲染逻辑.

值得一提的是, 整个小程序启动进程只有这三行代码, render函数执行完毕启动进程即宣告结束. 后续 render 中的 react-reconciler 会接管jsx对象的 setState 方法, 从而可以接管组件中的所有变动, 进而和旧 jsx 对象进行比较, 计算虚拟 Dom 变更情况, 生成实际 Dom 操作指令, 然后再根据 HostConfig 协议调用 Container 对象上暴露的方法…

HostConfig 协议和 Container 对象的实现我们放在下篇文章, 这篇文章我们只搞清楚两件事:

  1. render 函数的实现
  2. react-reconciler 接管 JSX 变更的实现

render 函数的实现

先看下 render 函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 位于 https://github.dev/remaxjs/remax/blob/v2.15.0/packages/remax-runtime/src/render.ts
import * as React from "react";
import ReactReconciler from "react-reconciler";
import hostConfig from "./hostConfig";
import Container from "./Container";
import AppContainer from "./AppContainer";

export const ReactReconcilerInst = ReactReconciler(hostConfig as any);

if (process.env.NODE_ENV === "development") {
ReactReconcilerInst.injectIntoDevTools({
bundleType: 1,
version: "16.13.1",
rendererPackageName: "remax",
});
}

function getPublicRootInstance(container: ReactReconciler.FiberRoot) {
const containerFiber = container.current;
if (!containerFiber.child) {
return null;
}
return containerFiber.child.stateNode;
}

export default function render(
rootElement: React.ReactElement | null,
container: Container | AppContainer
) {
// Create a root Container if it doesnt exist
if (!container._rootContainer) {
container._rootContainer = ReactReconcilerInst.createContainer(
container,
0,
false,
null
);
}

ReactReconcilerInst.updateContainer(
rootElement,
container._rootContainer,
null,
() => {
// ignore
}
);

return getPublicRootInstance(container._rootContainer);
}

可以看到, render 函数实际是对ReactReconciler的封装. 整个实现可以分为三步:

  1. 基于 HostConfig 初始化ReactReconcilerInst对象, 后续ReactReconciler会根据 HostConfig 提供的 API 生成 Dom 操作指令, 然后按照指令调用container上的接口
  2. 通过ReactReconcilerInst.createContainer方法将container对象包装为 Fiber 节点
  3. 通过ReactReconcilerInst.updateContainer方法获取待渲染的 JSX 对象

至此, 整个流程执行完毕. 为ReactReconciler输入HostConfig&container&JSX, ReactReconciler会启动对JSX的渲染, 并根据JSX对象的变动计算虚拟 Dom 的变更, 生成实际 Dom 更新指令并根据 HostConfig 配置调用 container 上的方法.

但这里存在一个问题了, JSX只是一个普普通通的 React.Component 对象, 状态变更调用的也是内部的 setState 方法, ReactReconciler是怎么知到JSX的变动状态并计算虚拟 Dom 变更的呢?

实际情况是ReactReconcilerupdateContainer方法中, 替换了JSX对象中 setState 方法的实现. 因此可以获知JSX的所有变动情况, 并根据需要调用JSX的生命周期钩子, 获取状态更新后的 render 结果.

不过说归说, talk is cheap show me your code. 接下来还是要依次看下 createContainer 和 updateContainer 的实现, 这里要涉及 react 的源码, 我们以react@16.7.0为例

ReactReconciler.createContainer 的实现

首先是 createContainer

1
2
3
4
5
6
7
8
9
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#L274
export function createContainer(
containerInfo: Container,
isConcurrent: boolean,
hydrate: boolean
): OpaqueRoot {
// 如果追下去的话会发现真的只初始化了一个FiberRoot, 其他啥都没干.
return createFiberRoot(containerInfo, isConcurrent, hydrate);
}

可以看到, 初始化容器只是简单创建了一个 Fiber 节点并返回, 本身没有多余操作

ReactReconciler.updateContainer 的实现

然后看看 updateContainer 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#L282

export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function
): ExpirationTime {
const current = container.current;
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, current);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
callback
);
}

updateContainer 主要工作就是将jsx对象container传给updateContainerAtExpirationTime, 并注册更新任务. 如果继续跟进的话, 可以看到以下调用链

1
2
3
4
5
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#L161
updateContainerAtExpirationTime{
// ...省略其余代码
return scheduleRootUpdate(current, element, expirationTime, callback);
}

=>

1
2
3
4
5
6
7
8
9
10
11
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#L161
export function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
expirationTime: ExpirationTime,
callback: ?Function
) {
// ...省略其余代码
return scheduleRootUpdate(current, element, expirationTime, callback);
}

=>

1
2
3
4
5
6
7
8
9
10
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#114
function scheduleRootUpdate(
current: Fiber,
element: ReactNodeList,
expirationTime: ExpirationTime,
callback: ?Function
) {
// ...省略其他代码
scheduleWork(current, expirationTime);
}

=>

1
2
3
4
5
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberScheduler.js#L1788
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
requestWork(root, rootExpirationTime);
// ...省略其他代码
}

requestWork对应的是注册组件更新任务代码, 如果继续跟下去的话, 会依次看到下边的调用链, 一直到beginWork

requestWork=>performWorkOnRoot=>renderRoot=>workLoop => performUnitOfWork => beginWork

看下beginWork的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberBeginWork.js#L1673
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime
): Fiber | null {
// ...省略其他代码
switch (workInProgress.tag) {
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime
);
}
}
// ...省略其他代码
}

对于函数组件, ReactReconciler 调用的是updateFunctionComponent函数, 对于类组件, ReactReconciler 调用的是updateClassComponent

至此, render 函数的原理讲解完毕. 接下来是那个核心问题: ReactReconciler是怎么拿到JSX的状态变更的.

ReactReconciler 获取 JSX 对象状态变更信息的实现

类组件: ClassComponent

先从类组件开始.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberBeginWork.js#L531
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps,
renderExpirationTime: ExpirationTime
) {
// ...省略其他代码
constructClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime
);
}

updateClassComponent中无门需要关注的是constructClassInstance, 将类组件实例化

1
2
3
4
5
6
7
8
9
10
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberClassComponent.js#513
function constructClassInstance(
workInProgress: Fiber,
ctor: any,
props: any,
renderExpirationTime: ExpirationTime
): any {
// ...省略其他代码
adoptClassInstance(workInProgress, instance);
}

需要关注的是adoptClassInstance, 在这个函数中, 将组件实例的updater设置为了classComponentUpdater

1
2
3
4
5
6
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberClassComponent.js#L503
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
// 关键代码
instance.updater = classComponentUpdater;
// ...省略其他代码
}

而这个classComponentUpdater, 其代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberClassComponent.js#L188
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);

const update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, "setState");
}
update.callback = callback;
}

flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
// ...省略其他代码
};

由于classComponentUpdaterReactReconciler提供, 所以对classComponentUpdater自然可以被ReactReconciler捕获到.

但为什么将组件实例的updater设置成classComponentUpdater就会被捕获呢? 搂一眼React.Component的源码

1
2
3
4
5
6
7
8
9
10
11
12
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react/src/ReactBaseClasses.js#L58

Component.prototype.setState = function (partialState, callback) {
invariant(
typeof partialState === "object" ||
typeof partialState === "function" ||
partialState == null,
"setState(...): takes an object of state variables to update or a " +
"function which returns an object of state variables."
);
this.updater.enqueueSetState(this, partialState, callback, "setState");
};

显然, Component中的 setState 实际上调用的就是 updater 上的enqueueSetState方法. 而由于 updater 本身已经被替换为了ReactReconciler自身的实现, 所以自然可以捕获到类组件上的所有数据变更.

问题得解

函数组件: FunctionComponent

接着看下一项, ReactReconciler对函数组件中 useState 的接管实现

1
2
3
4
5
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react/src/ReactHooks.js#L54
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}

useState 位于ReactHooks.js文件, 实际调用的是ReactCurrentOwner.currentDispatcher上提供的 useState 方法

1
2
3
4
5
6
7
8
9
10
11
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react/src/ReactHooks.js#L14
import ReactCurrentOwner from "./ReactCurrentOwner";

function resolveDispatcher() {
const dispatcher = ReactCurrentOwner.currentDispatcher;
invariant(
dispatcher !== null,
"Hooks can only be called inside the body of a function component."
);
return dispatcher;
}

resolveDispatcher返回的又是ReactCurrentOwner.currentDispatcher对象. 这个ReactCurrentOwner看起来位于packages/react/src/ReactCurrentOwner.js, 但点进去会发现里边只有一个普通对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react/src/ReactCurrentOwner.js#L1
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import typeof {Dispatcher} from 'react-reconciler/src/ReactFiberDispatcher';

/**
* Keeps track of the current owner.
*
* The current owner is the component who should own any components that are
* currently being constructed.
*/
const ReactCurrentOwner = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Fiber),
currentDispatcher: (null: null | Dispatcher),
}

export default ReactCurrentOwner;

所以react/src/ReactCurrentOwner.js显然不是ReactCurrentOwner实际的提供者. 如果返回beginWork, 看ReactReconciler提供ReactCurrentOwner的方式时我们会看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberBeginWork.js#L47
// ...省略其他代码
import ReactSharedInternals from "shared/ReactSharedInternals";
// ...省略其他代码
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
// ...省略其他代码
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderExpirationTime
) {
// ...省略其他代码
}

ReactReconciler也提供了一个ReactCurrentOwner, 如果继续往后跟, 可以看到他在workLoop中替换了ReactCurrentOwner.currentDispatcher

1
2
3
4
5
6
7
8
9
10
11
12
13
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberScheduler.js#29
import ReactSharedInternals from "shared/ReactSharedInternals";
// ...省略其他代码
const { ReactCurrentOwner } = ReactSharedInternals;
// 位于 https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberScheduler.js#1187
function workLoop(isYieldy) {
// ...省略其他代码
if (enableHooks) {
ReactCurrentOwner.currentDispatcher = Dispatcher;
} else {
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
}
}

但问题是, ReactReconciler引入的是shared/ReactSharedInternals, react 中引用的却是react/src/ReactCurrentOwner.js, 这是怎么做到的?

来看这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 位于 https://github.com/facebook/react/blob/v16.7.0/scripts/rollup/forks.js#L48

// Without this fork, importing `shared/ReactSharedInternals` inside
// the `react` package itself would not work due to a cyclical dependency.
'shared/ReactSharedInternals': (bundleType, entry, dependencies) => {
if (entry === 'react') {
return 'react/src/ReactSharedInternals';
}
if (dependencies.indexOf('react') === -1) {
// React internals are unavailable if we can't reference the package.
// We return an error because we only want to throw if this module gets used.
return new Error(
'Cannot use a module that depends on ReactSharedInternals ' +
'from "' +
entry +
'" because it does not declare "react" in the package ' +
'dependencies or peerDependencies. For example, this can happen if you use ' +
'warning() instead of warningWithoutStack() in a package that does not ' +
'depend on React.'
);
}
return null;
},

显然, 答案是 rollup.

react 在使用 rollup 构建时, 通过定制编译脚本, 在输出将shared/ReactSharedInternals映射为了react/src/ReactSharedInternals, 从而实现对ReactCurrentOwner变量的替换, 进而将 useState 的实际提供者替换为ReactReconciler, 实现了对 useState 的控制

而我们对ReactReconciler接管函数组件useState的过程, 也可以宣告结束.

搞定了ReactReconciler的秘密, 在接下来的文章里, 我们就可以放心的研究 HostConfig 和 Container 的设计和实现了

参考资料

小前端读源码 - React 组件更新原理
react 源码剖析:react/react-dom/react-reconciler 的关系


小程序架构指南(四):源码详解小程序启动原理(上)
https://www.yaozeyuan.online/2021/11/08/2021/11/04-源码详解_小程序启动原理(上)-源码级小程序启动流程与React-Reconciler监控组件状态的实现/04-源码详解_小程序启动原理(上)-源码级小程序启动流程与React-Reconciler监控组件状态的实现/
作者
姚泽源
发布于
2021年11月8日
许可协议