上回说到, 借助 React Fiber 架构提供的能力, 我们可以基于 React 完成小程序架构. 但由于篇幅所限, 我们只概要描述了下思路而略过了核心原理和实现方案. 在这篇文章中, 我们会以基于同样构建思路的remax@2.15.0 为例, 分析类小程序项目中项目的具体启动过程.
通过之前的文章我们知道, 小程序的基本启动模型是:
解析 app.json, 获取其中注册的JSX
对象和对应的 path
初始化实现了HostConfig
协议所约定接口的对象, 作为负责实际渲染的容器Container
获取待渲染的JSX
对象
从 Native 中获取当前打开的 scheme, 解析出正在访问的路径&参数
和已注册路由进行比较
如果匹配到已注册 path, 则加载对应的JSX
对象
否则加载默认页面对应的JSX
对象
[可选]如果没找到匹配路径, 也可以直接报白屏错误, 看小程序引擎实现者的心情
将Container
对象, 和JSX
对象 一起传入由Reconciler
导出的render
方法
在传统浏览器环境中
Reconciler
会将JSX
渲染为虚拟 Dom
期间根据JSX
变动, 不断产生更新指令, 将指令转换为HostConfig
中约定的 Dom 操作, 并调用Container
暴露的操作方法.
Container
根据被调用的操作, 创建实际 Dom. 从而生成实际页面
在实际小程序运行环境中
由于小程序环境中逻辑层和渲染层分开展示, 因此在逻辑层中运行的Container
并不会创建实际 Dom.
所以在小程序应用中, 我们引入一个中间层, 用 js 对象模拟 Dom 操作, 并记录Reconciler
传入的 Dom 操作指令.
在一个操作批次结束后, 将操作指令 json 化, 变成字符串格式的指令列表
通过Native
转发给位于渲染层的webview-render
对象
webview-render
对象根据操作指令, 在 webview 中构建实际 Dom
也就是这个模型
ReactElement对象 -> Render(React-Reconciler) -> Container(HostConfig) -> 转发命令 -> Webview-Render
我们以Remax@2.15.0
和React@16.7.0
为例, 结合实际代码对启动流程进行一次跟踪
小程序启动示例代码如下所示
1 2 3 4 5 6 7 8 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);
在这段代码中, 我们完成了以下工作:
直接获取待渲染的 jsx 对象 MiniProgramPage
在逻辑层内初始化 Dom 容器 Container
, 用于在 js-core 中模拟 Dom 功能, 接收并缓存后续ReactReconciler
传过来的 Dom 指令
将 jsx对象
和Container
传给 render, 进入渲染逻辑.
值得一提的是, 整个小程序启动进程只有这三行代码, render
函数执行完毕启动进程即宣告结束. 后续 render 中的 react-reconciler 会接管jsx对象
的 setState 方法, 从而可以接管组件中的所有变动, 进而和旧 jsx 对象进行比较, 计算虚拟 Dom 变更情况, 生成实际 Dom 操作指令, 然后再根据 HostConfig 协议调用 Container 对象上暴露的方法…
HostConfig 协议和 Container 对象的实现我们放在下篇文章, 这篇文章我们只搞清楚两件事:
render 函数的实现
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 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 ) { if (!container._rootContainer ) { container._rootContainer = ReactReconcilerInst .createContainer ( container, 0 , false , null ); } ReactReconcilerInst .updateContainer ( rootElement, container._rootContainer , null , () => { } ); return getPublicRootInstance (container._rootContainer ); }
可以看到, render 函数实际是对ReactReconciler
的封装. 整个实现可以分为三步:
基于 HostConfig 初始化ReactReconcilerInst
对象, 后续ReactReconciler
会根据 HostConfig 提供的 API 生成 Dom 操作指令, 然后按照指令调用container
上的接口
通过ReactReconcilerInst.createContainer
方法将container
对象包装为 Fiber 节点
通过ReactReconcilerInst.updateContainer
方法获取待渲染的 JSX
对象
至此, 整个流程执行完毕. 为ReactReconciler
输入HostConfig
&container
&JSX
, ReactReconciler
会启动对JSX
的渲染, 并根据JSX
对象的变动计算虚拟 Dom 的变更, 生成实际 Dom 更新指令并根据 HostConfig 配置调用 container 上的方法.
但这里存在一个问题了, JSX
只是一个普普通通的 React.Component
对象, 状态变更调用的也是内部的 setState 方法, ReactReconciler
是怎么知到JSX
的变动状态并计算虚拟 Dom 变更的呢?
实际情况是ReactReconciler
在updateContainer
方法中, 替换了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 export function createContainer ( containerInfo: Container, isConcurrent: boolean , hydrate: boolean ): OpaqueRoot { 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 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 updateContainerAtExpirationTime{ return scheduleRootUpdate (current, element, expirationTime, callback); }
=>
1 2 3 4 5 6 7 8 9 10 11 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 function scheduleRootUpdate ( current: Fiber, element: ReactNodeList, expirationTime: ExpirationTime, callback: ?Function ) { scheduleWork (current, expirationTime); }
=>
1 2 3 4 5 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 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 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 function constructClassInstance ( workInProgress: Fiber, ctor: any , props: any , renderExpirationTime: ExpirationTime ): any { adoptClassInstance (workInProgress, instance); }
需要关注的是adoptClassInstance
, 在这个函数中, 将组件实例的updater
设置为了classComponentUpdater
1 2 3 4 5 6 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 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); }, };
由于classComponentUpdater
由ReactReconciler
提供, 所以对classComponentUpdater
自然可以被ReactReconciler
捕获到.
但为什么将组件实例的updater
设置成classComponentUpdater
就会被捕获呢? 搂一眼React.Component
的源码
1 2 3 4 5 6 7 8 9 10 11 12 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 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 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 import type {Fiber } from 'react-reconciler/src/ReactFiber' ;import typeof {Dispatcher } from 'react-reconciler/src/ReactFiberDispatcher' ;const ReactCurrentOwner = { 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 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 import ReactSharedInternals from "shared/ReactSharedInternals" ;const { ReactCurrentOwner } = ReactSharedInternals ;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 'shared/ReactSharedInternals' : (bundleType, entry, dependencies ) => { if (entry === 'react' ) { return 'react/src/ReactSharedInternals' ; } if (dependencies.indexOf ('react' ) === -1 ) { 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 的关系