Taro 运行时机制解析
引言
本文旨在探索 Taro 如何处理运行时,特别是如何将 React 组件渲染到小程序环境中。核心问题是:Taro 如何构建出小程序所需的 App、Page 结构,以及如何实现 React 组件树到小程序模板树的映射。
小程序的基本结构
小程序运行需要特定的结构和 API。首先了解小程序的基本结构:
App 结构
App({
onLaunch() {}
})
Page 结构
Page({
data: {},
onLoad() {},
onShow() {},
onReady() {},
})
小程序运行需要上面的这种结构和 API。所以 Taro 工程在编译后应该也具备 App、Page 这些内容。这里的关键要看 Taro 如何构建出 App、Page 等内容。
小程序模板示例
Page({
data: {
demo: {
motto: 'Hello World',
oasis: '!!!'
}
},
})
<template name="odd">
<view> {{ demo.motto }} </view>
</template>
<template name="even">
<view> {{ demo.oasis }} </view>
</template>
<block wx:for="{{[1, 2, 3, 4, 5]}}">
<template is="{{item % 2 == 0 ? 'even' : 'odd'}}" data="{{ demo }}"/>
</block>
Taro 编译后的运行情况
Root 数据结构
源项目中的 pages/index.jsx 中的元素嵌套关系如下:

编译后的 root 数据结构:

const root = {
cn: [
{
cl: 'index',
cn: [
{
cn: [
{
nn: '8',
sid: '_AG',
v: 'Hello world!'
}
],
nn: '4',
sid: '_AH'
}
],
nn: '2',
sid: '_AI'
}
]
}
这里可以看做虚拟节点树,实际最终要把这棵树的渲染映射到小程序 xml 模板上。
用最简化的例子来看,pages 下的 index 页面中的 xml 文件加载 base.xml 中的模板,采用编译后的数据 root,进行数据的传递,在通过 xs 计算映射的模板后,最终映射到 tmpl_0_8:
<template name="tmpl_0_8">
<block>{{i.v}}</block>
</template>
渲染触发的入口
实际编译后小程序项目中 pages/index.js 文件内容:
import { createPageConfig } from '@tarojs/runtime'
import component from "!!../../../node_modules/.pnpm/@tarojs+taro-loader@4.0.0_webpack@5.91.0_@swc+core@1.3.96_/node_modules/@tarojs/taro-loader/lib/entry-cache.js?name=pages/index/index!./index.jsx"
var config = {"navigationBarTitleText":"首页"};
var inst = Page(createPageConfig(component, 'pages/index/index', {root:{cn:[]}}, config || {}))
export default component
createPageConfig 来自 @tarojs/runtime,这里创建 page json 的静态结构。在这个步骤中,只初始化了 root 数据,实际的数据处理在哪?这个问题比较重要。
useState 来自 webpack/container/remote/react,这里在前面创建的数据基础上,由 React 来驱动数据更新。
这里既要考虑小程序渲染模板树的收集,又要考虑 React 如何驱动渲染:
- 小程序渲染树 root 的收集:主要看 React 渲染机制如何在小程序渲染机制的上层建立
- React 如何驱动数据更新:主要看与 root 数据的关系,React JSX 与小程序 template 的关系
Taro 的架构设计
自定义渲染器
Taro 小程序采用了 React 创建组件,但是渲染采用了基于 react-reconciler 创建的自定义渲染器:
const TaroReconciler = Reconciler(hostConfig)
TaroReconciler 在封装 render 方法时候,会在 taro-framework-react 的 connect.ts 中调用。createReactApp 设置 setReconciler 层的目的是实现解耦,让 React 层插件化。
平台适配
taro-platform-weapp 会重新设计 react-reconciler:
import { mergeInternalComponents, mergeReconciler } from '@tarojs/shared'
import { components, hostConfig } from './runtime-utils'
mergeReconciler(hostConfig)
mergeInternalComponents(components)
taro-framework-react 中会把 React 相关的内容进行 alias 映射:
alias.set('react-reconciler$', 'react-reconciler/cjs/react-reconciler.production.min.js')
alias.set(/^(?!.*mobx-react$).*react$/, newFilePath)
alias.set('react/jsx-runtime$', 'react/cjs/react-jsx-runtime.production.min.js')
Taro3 的解释型架构
Taro3 可以大致理解为解释型架构,这个工作主要是在运行时”对代码进行解释”。
升级为 Taro3 后,你会发现 package.json 文件里面多了个 @taro/runtime 的依赖。打开包所在目录,会惊奇的发现我们在 web 中才会有的 BOM 跟 DOM 相关的关键字。原来 Taro3 自己实现了一套类似浏览器的 BOM/DOM 那一套 API,通过 webpack 的 plugin 注入到小程序的逻辑层。
打包编译后,你最终的代码都是基于 BOM/DOM 这几个 API 来实现你的具体功能。不管什么平台,都有自己一套元素 dom 的规则,都有各自平台的类似 bom 的全局 api 规则,Taro3 做的就是整合这些厂家的规则封装为类似 BOM/DOM 的思想去用。也就是说,我不管你开发时用的什么框架,我只要保证你运行时能帮你适配到各个平台即可。
这样做的最直观的好处就是,不再受限制与框架本身了。理论上来说,Taro 不仅可以支持 Vue 和 React,也能用 jQuery、Angular 等等的库进行跨端开发。
站在 React 的使用者角度:通过 taro2 和 taro3 两个项目开发经验来说,还有一点最直观的感受,taro3 中写 JSX 更加舒服了!其实就更加友好的支持 JSX 这一点,应该是顺理成章的,因为 taro 的架构其实就是无限接近于 React 的开发体验,适配的工作是通过运行时的 BOM/DOM 去完成的,而不是像之前版本一样,通过穷举的方式对 JSX 的写法进行适配。
关键点:既然 Taro3 自己实现了 BOM/DOM 这一套 API,而 React 中的渲染器,如 react-dom 中调用的是浏览器的 BOM/DOM 的 API,那 Taro 肯定会有自己一套渲染器来链接 React 的协调器(reconciler,diff 算法所在阶段)和 taro-runtime 的 BOM/DOM API。源码路径:@tarojs/react,description 里面的描述如下:”like react-dom, but for mini apps.”
数据流概览
- Root 数据的初始化在
createReactApp中完成 - 数据更新由
TaroRootElement负责管理 - 实际渲染由 ReactDOM 的 Root 类处理
- 最终通过小程序的
setData实现真实 DOM 更新
核心机制详解
createReactApp
createReactApp 方法是 Taro 框架的一个核心方法,用于创建 React 应用并将其适配到小程序环境。
主要功能和实现细节
初始化设置: - 接收主要参数:App 组件、React 实例、ReactDOM 实例和配置对象 - 设置 React 相关的全局引用 - 创建应用实例的引用
AppWrapper 核心组件:
- 继承自 React.Component
- 管理页面的生命周期
- 维护页面数组和元素数组
- 提供页面挂载和卸载的方法
页面管理:
- mount 方法:负责新页面的挂载
- unmount 方法:处理页面的卸载
- 使用 connectReactPage 连接 React 组件与小程序页面
渲染机制: - 使用 React 18 的新 API 或降级使用旧版本的渲染方式 - 处理页面栈的管理和渲染 - 支持异步的页面加载和卸载
生命周期适配: - 将 React 的生命周期与小程序的生命周期进行对接 - 处理页面切换和状态管理
这个实现的主要目的是: - 让 React 组件能够在小程序环境中正常运行 - 处理好小程序的页面栈管理 - 确保 React 组件的生命周期能够正确响应小程序的事件 - 提供统一的页面管理机制
通过这种方式,开发者可以使用熟悉的 React 组件开发方式来开发小程序,而 Taro 框架则负责处理底层的适配工作。
TaroRootElement 数据收集机制
TaroRootElement 中的 performUpdate 会收集 data 数据,并调用小程序的 setData 更新数据。
数据收集流程
1. TaroNode 类中的基础节点操作
TaroNode = class _TaroNode extends TaroEventTarget {
updateChildNodes(isClean) {
const cleanChildNodes = () => [];
const rerenderChildNodes = () => {
const childNodes = this.childNodes.filter((node) => !isComment(node));
return childNodes.map(hydrate);
};
this.enqueueUpdate({
path: `${this._path}.${CHILDNODES}`,
value: isClean ? cleanChildNodes : rerenderChildNodes
});
}
}
这里定义了 updateChildNodes 方法,它负责收集和更新子节点数据。当子节点发生变化时,会调用 hydrate 方法处理每个子节点。
2. hydrate 方法
function hydrate(node) {
var _a2;
componentsAlias2 || (componentsAlias2 = getComponentsAlias2());
SPECIAL_NODES || (SPECIAL_NODES = _chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_0__.hooks.call("getSpecialNodes"));
const nodeName = node.nodeName;
let compileModeName = null;
if (isText(node)) {
return {
sid: node.sid,
["v"]: node.nodeValue,
["nn"]: ((_a2 = componentsAlias2[nodeName]) === null || _a2 === void 0 ? void 0 : _a2._num) || "8"
};
}
const data = {
["nn"]: nodeName,
sid: node.sid
};
if (node.uid !== node.sid) {
data.uid = node.uid;
}
if (!node.isAnyEventBinded() && SPECIAL_NODES.indexOf(nodeName) > -1) {
data["nn"] = `static-${nodeName}`;
if (nodeName === VIEW && !isHasExtractProp(node)) {
data["nn"] = PURE_VIEW;
}
}
const { props } = node;
// ...
}
这个方法负责将节点转换为小程序可以理解的数据格式,包括: - 节点 ID(sid) - 节点名称(nn) - 节点值(v) - 特殊节点的处理
3. 节点的插入和删除操作
insertBefore(newChild, refChild, isReplace) {
if (newChild.nodeName === DOCUMENT_FRAGMENT) {
// ...
}
if (this._root) {
if (!refChild) {
const isOnlyChild = childNodesLength === 1;
if (isOnlyChild) {
this.updateChildNodes();
} else {
this.enqueueUpdate({
path: newChild._path,
value: this.hydrate(newChild)
});
}
} else if (isReplace) {
this.enqueueUpdate({
path: newChild._path,
value: this.hydrate(newChild)
});
} else {
const mark = childNodesLength * 2 / 3;
if (mark > index) {
this.updateChildNodes();
} else {
this.updateSingleChild(index);
}
}
}
MutationObserver2.record({
type: "childList",
target: this,
addedNodes: [newChild],
removedNodes: isReplace ? [refChild] : [],
nextSibling: isReplace ? refChild.nextSibling : refChild || null,
previousSibling: newChild.previousSibling
});
return newChild;
}
在 insertBefore 方法中,会根据不同情况选择更新策略:
- 如果是唯一子节点,更新整个子节点树
- 如果有参考节点,更新单个节点
- 根据节点位置决定是更新整个树还是单个节点
4. 数据更新的入队
enqueueUpdate(payload) {
var _a2;
(_a2 = this._root) === null || _a2 === void 0 ? void 0 : _a2.enqueueUpdate(payload);
}
所有节点的更新都会通过 enqueueUpdate 方法进入更新队列。
5. 最终在 performUpdate 中处理更新
performUpdate(initRender = false, prerender) {
this.pendingUpdate = true;
const ctx = _chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_0__.hooks.call("proxyToRaw", this.ctx);
setTimeout(() => {
const setDataMark = `${SET_DATA} 开始时间戳 ${Date.now()}`;
perf.start(setDataMark);
const data = /* @__PURE__ */ Object.create(null);
const resetPaths = new Set(initRender ? [
"root.cn.[0]",
"root.cn[0]"
] : []);
while(this.updatePayloads.length > 0) {
const { path, value } = this.updatePayloads.shift();
if (path.endsWith("cn")) {
resetPaths.add(path);
}
data[path] = value;
}
for(const path in data) {
resetPaths.forEach((p) => {
if (path.includes(p) && path !== p) {
delete data[path];
}
});
const value = data[path];
if ((0,_chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_0__.isFunction)(value)) {
data[path] = value();
}
}
if ((0,_chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_0__.isFunction)(prerender)) return prerender(data);
this.pendingUpdate = false;
let normalUpdate = {};
const customWrapperMap = /* @__PURE__ */ new Map();
if (initRender) {
normalUpdate = data;
} else {
for(const p in data) {
const dataPathArr = p.split(".");
const found = findCustomWrapper(this, dataPathArr);
if (found) {
const { customWrapper, splitedPath } = found;
customWrapperMap.set(customWrapper, Object.assign(Object.assign({}, customWrapperMap.get(customWrapper) || {}), {
[`i.${splitedPath}`]: data[p]
}));
} else {
normalUpdate[p] = data[p];
}
}
}
const customWrapperCount = customWrapperMap.size;
const isNeedNormalUpdate = Object.keys(normalUpdate).length > 0;
const updateArrLen = customWrapperCount + (isNeedNormalUpdate ? 1 : 0);
let executeTime = 0;
const cb = () => {
if (++executeTime === updateArrLen) {
perf.stop(setDataMark);
this.flushUpdateCallback();
initRender && perf.stop(PAGE_INIT);
}
};
if (customWrapperCount) {
customWrapperMap.forEach((data2, ctx2) => {
if (options.debug) {
console.log("custom wrapper setData: ", data2);
}
ctx2.setData(data2, cb);
});
}
if (isNeedNormalUpdate) {
if (options.debug) {
console.log("page setData:", normalUpdate);
}
ctx.setData(normalUpdate, cb);
}
}, 0);
}
这个方法会:
- 收集所有待更新的数据
- 处理更新路径
- 区分普通更新和自定义组件更新
- 最终通过 setData 更新到视图层
整个过程形成了一个完整的数据收集链路:
- 节点变化触发更新
- 更新进入队列
- hydrate 处理节点数据
- performUpdate 统一处理更新
- setData 更新到视图层
这样的设计确保了 React 组件树的变化能够被正确地收集和同步到小程序的数据层。
createReactApp 和 Page 中的数据通信
从代码中可以看出,createReactApp 和 Page 的数据通信主要通过以下几个关键步骤:
1. 在 createReactApp 中创建 AppWrapper 组件,它负责管理所有页面:
class AppWrapper extends react.Component {
mount(pageComponent, id, cb) {
const pageWrapper = connectReactPage(react, id)(pageComponent);
const key = id + pageKeyId();
const page = () => h$1(pageWrapper, {
key,
tid: id
});
this.pages.push(page);
this.forceUpdate((...args) => {
_chunk_R2V3WQ7B_js__WEBPACK_IMPORTED_MODULE_0__.perf.stop(_chunk_R2V3WQ7B_js__WEBPACK_IMPORTED_MODULE_0__.PAGE_INIT);
return cb(...args);
});
}
unmount(id, cb) {
const elements = this.elements;
const idx = elements.findIndex((item) => item.props.tid === id);
elements.splice(idx, 1);
this.forceUpdate(cb);
}
render() {
const { pages, elements } = this;
while(pages.length > 0) {
const page = pages.pop();
elements.push(page());
}
// ...
}
}
2. 当页面需要挂载时,通过 connectReactPage 创建页面包装器:
function connectReactPage(R, id) {
return (Page) => {
const isReactComponent = isClassComponent(R, Page);
const inject = (node) => node && (0,_chunk_R2V3WQ7B_js__WEBPACK_IMPORTED_MODULE_0__.injectPageInstance)(node, id);
const refs = isReactComponent ? {
ref: inject
} : {
forwardedRef: inject,
// 兼容 react-redux 7.20.1+
reactReduxForwardedRef: inject
};
if (reactMeta.PageContext === _chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_1__.EMPTY_OBJ) {
reactMeta.PageContext = R.createContext("");
}
return class PageWrapper extends R.Component {
static getDerivedStateFromError(error) {
var _a, _b;
(_b = (_a = _chunk_R2V3WQ7B_js__WEBPACK_IMPORTED_MODULE_0__.Current.app) === null || _a === void 0 ? void 0 : _a.onError) === null || _b === void 0 ? void 0 : _b.call(_a, error.message + error.stack);
return {
hasError: true
};
}
// React 16 uncaught error 会导致整个应用 crash,
// 目前把错误缩小到页面
componentDidCatch(error, info) {
if (true) {
console.warn(error);
console.error(info.componentStack);
}
}
render() {
const children = this.state.hasError ? [] : h$1(reactMeta.PageContext.Provider, {
value: id
}, h$1(Page, Object.assign(Object.assign({}, this.props), refs)));
if (false) {} else {
return h$1("root", {
id
}, children);
}
}
constructor() {
super(...arguments);
this.state = {
hasError: false
};
}
};
};
}
3. 页面配置通过 createPageConfig 创建,在这里处理页面的生命周期和数据更新:
function createPageConfig(component, pageName, data, pageConfig) {
const id = pageName !== null && pageName !== void 0 ? pageName : `taro_page_${pageId()}`;
const [ONLOAD, ONUNLOAD, ONREADY, ONSHOW, ONHIDE, LIFECYCLES, SIDE_EFFECT_LIFECYCLES] = _chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_0__.hooks.call("getMiniLifecycleImpl").page;
let pageElement = null;
let unmounting = false;
let prepareMountList = [];
function setCurrentRouter(page) {
const router = false ? 0 : page.route || page.__route__ || page.$taroPath;
Current.router = {
params: page.$taroParams,
path: addLeadingSlash(router),
$taroPath: page.$taroPath,
onReady: getOnReadyEventKey(id),
onShow: getOnShowEventKey(id),
onHide: getOnHideEventKey(id)
};
if (!(0,_chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_0__.isUndefined)(page.exitState)) {
Current.router.exitState = page.exitState;
}
}
let loadResolver;
let hasLoaded;
const config = {
[ONLOAD] (options2 = {}, cb) {
hasLoaded = new Promise((resolve) => {
loadResolver = resolve;
});
perf.start(PAGE_INIT);
Current.page = this;
this.config = pageConfig || {};
const uniqueOptions = Object.assign({}, options2, {
$taroTimestamp: Date.now()
});
const $taroPath = this.$taroPath = getPath(id, uniqueOptions);
if (this.$taroParams == null) {
this.$taroParams = uniqueOptions;
}
setCurrentRouter(this);
if (true) {
window2.trigger(CONTEXT_ACTIONS.INIT, $taroPath);
}
const mount = () => {
Current.app.mount(component, $taroPath, () => {
pageElement = env.document.getElementById($taroPath);
(0,_chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_0__.ensure)(pageElement !== null, "没有找到页面实例。");
safeExecute($taroPath, ON_LOAD, this.$taroParams);
loadResolver();
if (true) {
pageElement.ctx = this;
pageElement.performUpdate(true, cb);
} else {}
});
};
if (unmounting) {
prepareMountList.push(mount);
} else {
mount();
}
}
};
// ...
}
主要通信流程
数据流向:
- AppWrapper 维护页面栈
- 通过 mount/unmount 方法管理页面的加载和卸载
- 页面实例通过 performUpdate 方法更新数据
- 最终通过小程序的 setData 实现数据同步
更新机制:
- 页面更新时会调用 TaroRootElement 的 performUpdate(见上一节)
事件处理:
- React 事件通过 unstable_batchedUpdates 处理批量更新
var unstable_batchedUpdates = (fn, a) => {
if (isInsideEventHandler) {
return fn(a);
}
isInsideEventHandler = true;
try {
return TaroReconciler.batchedUpdates(fn, a);
} finally {
isInsideEventHandler = false;
finishEventHandler();
}
};
整个通信过程形成了一个完整的链路:
- createReactApp 创建应用实例
- connectReactPage 连接 React 组件与小程序页面
- createPageConfig 处理页面配置和生命周期
- performUpdate 统一处理数据更新
- setData 同步到小程序层
这样的设计确保了 React 组件和小程序页面之间的数据能够保持同步,同时保持了各自的生命周期管理。
AppWrapper 页面栈管理
页面栈维护机制
1. AppWrapper 通过两个数组维护页面状态:
this.pages:存储待渲染的页面组件this.elements:存储已渲染的页面元素
2. 页面切换时的数据处理:
在 createPageConfig 中,通过 Current 对象维护当前页面的状态:
- Current.page:当前页面实例
- Current.router:当前路由信息
- Current.app:应用实例
3. 页面间共享数据的方式:
a. 通过 Current 对象:
function setCurrentRouter(page) {
const router = false ? 0 : page.route || page.__route__ || page.$taroPath;
Current.router = {
params: page.$taroParams,
path: addLeadingSlash(router),
$taroPath: page.$taroPath,
onReady: getOnReadyEventKey(id),
onShow: getOnShowEventKey(id),
onHide: getOnHideEventKey(id)
};
if (!(0,_chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_0__.isUndefined)(page.exitState)) {
Current.router.exitState = page.exitState;
}
}
Current 对象作为全局状态管理器,可以在不同页面间共享数据。
b. 通过 PageContext:
React 的 Context 机制用于跨组件传递数据。
if (reactMeta.PageContext === _chunk_KEK4XUFF_js__WEBPACK_IMPORTED_MODULE_1__.EMPTY_OBJ) {
reactMeta.PageContext = R.createContext("");
}
总结
Taro 的运行时机制通过以下几个核心部分实现了 React 到小程序的适配:
- 自定义渲染器:基于
react-reconciler创建,连接 React 协调器和小程序的 BOM/DOM API - 数据收集:通过
TaroRootElement和TaroNode收集 React 组件树的变化 - 数据同步:通过
performUpdate和setData将数据同步到小程序视图层 - 页面管理:通过
AppWrapper和createPageConfig管理小程序页面栈和生命周期 - 状态共享:通过
Current对象和PageContext实现页面间数据共享
这样的架构设计使得开发者可以使用熟悉的 React 开发方式,而 Taro 框架负责处理底层的适配工作,实现了”一次编写,多端运行”的目标。
