03 — 小程序模板、hydrate 与 setData
1. 模板体系的「两个维度」(源码注释即标准答案)
@tarojs/shared 的 template.ts 文件头把小程序宿主差异抽象成两类问题:
1:13:packages/shared/src/template.ts
/**
* 这里我们需要关心的小程序种类有两类:
* 1. 模板递归:
* - 支持:tmpl0 套 tmpl0
* - 不支持:这就使得我们必须生成多级的模板,tmpl0 套 tmpl1,tmpl1 套 tmpl2……
* 直到超过阈值 N (N = config.miniapp.baseLevel) tmplN 会套组件 comp,组件 comp 重新再套 tmpl0。
* 2. 小程序脚本语言(wxs, sjs, etc...):
* - 支持:可以在模板使用函数缩减模板大小或提高性能(存疑),例如判断一个值是不是假值(falsy value)。
* 将来或许会把数据序列化^1 的操作也放到小程序脚本语言里。
* - 不支持:使用纯 *xml 语法
*
* ^1: packages/taro-runtime/src/hydrate.ts
*/简明表述:
- 递归模板:微信等部分平台允许模板自引用;不支持时就必须 展开层级,用
baseLevel控制深度,深层用 自定义组件 comp 再切入新子树(控制模板体积与编译限制)。 - WXS/SJS:可在模板里做简单逻辑,减少 wxml 体积;不支持时退化为 三元表达式等纯模板语法(
isUseXS为 false 时的分支在createMiniComponents里可见)。
2. Adapter:把通用描述映射到具体小程序语法
同一套生成逻辑通过 IAdapter 映射到 wx:if、wx:for 或他端关键字:
44:77:packages/shared/src/template.ts
export interface IAdapter {
if: string
else: string
elseif: string
for: string
forItem: string
forIndex: string
key: string
xs?: string
type: string
}
const weixinAdapter: IAdapter = {
if: 'wx:if',
else: 'wx:else',
elseif: 'wx:elif',
for: 'wx:for',
forItem: 'wx:for-item',
forIndex: 'wx:for-index',
key: 'wx:key',
xs: 'wxs',
type: 'weapp'
}各 platform-* 包会替换 Adapter 与 internalComponents 差异,实现 「一套生成器,多端拼字符串」。
3. hydrate:从「类 DOM 树」到「小程序 data」
@tarojs/runtime 的 hydrate 注释直接对齐 React 的命名:遍历 + 改数据形状,把 TaroElement 树变成小程序 setData 可用的 扁平/短键 结构:
23:50:packages/taro-runtime/src/hydrate.ts
/**
* React also has a fancy function's name for this: `hydrate()`.
* You may have been heard `hydrate` as SSR-related function,
* actually, `hydrate` basicly do the `render()` thing, but ignore some properties,
* it's a vnode traverser and modifier: that's exactly what Taro's doing in here.
*/
export function hydrate (node: TaroElement | TaroText): MiniData {
// 初始化 componentsAlias
componentsAlias ||= getComponentsAlias()
// ...
if (isText(node)) {
return {
sid: node.sid,
[Shortcuts.Text]: node.nodeValue,
[Shortcuts.NodeName]: componentsAlias[nodeName]?._num || '8'
}
}设计原因:
- 小程序视图层只认 模板 + data,没有浏览器 DOM;Runtime 必须提供 稳定、可 diff 的数据协议(
Shortcuts缩短 key 长度也是常见优化手段,见shared的shortcuts)。
4. setData 调度:TaroRootElement
根节点 enqueueUpdate 收集 payload,在 performUpdate 里合并后调用小程序 ctx.setData。注意 scheduleTask 选用 setTimeout 而非微任务 的注释——这是实践里对 连续 setData 与 时机 的取舍:
78:112:packages/taro-runtime/src/dom/root.ts
public scheduleTask(fn: TFunc) {
// 这里若使用微任务可略微提前setData的执行时机,但在部分场景下可能会出现连续setData两次,造成更大的性能问题
setTimeout(fn)
}
public enqueueUpdate (payload: UpdatePayload): void {
this.updatePayloads.push(payload)
if (!this.pendingUpdate && this.ctx) {
this.performUpdate()
}
}
public performUpdate (initRender = false, prerender?: TFunc) {
this.pendingUpdate = true
// ...
while (this.updatePayloads.length > 0) {
const { path, value } = this.updatePayloads.shift()!
if (path.endsWith(Shortcuts.Childnodes)) {
resetPaths.add(path)
}
data[path] = value
}后续同文件还会处理 CustomWrapper 路径:把更新拆到 自定义组件 上,减少整页 setData 数据量(大列表场景常见优化)。
5. 平台钩子:hostConfig 示例(微信)
platform-weapp 的 runtime-utils 导出 hostConfig,通过 mergeReconciler 把平台特有生命周期、混合原生时的 transferHydrateData 等注入运行时(与 plugin-inject 同类机制):
10:33:packages/taro-platform-weapp/src/runtime-utils.ts
export const hostConfig = {
initNativeApi,
getMiniLifecycle (config) {
const methods = config.page[5]
if (methods.indexOf('onSaveExitState') === -1) {
methods.push('onSaveExitState')
}
return config
},
transferHydrateData (data, element, componentsAlias) {
if (element.isTransferElement) {
const pages = getCurrentPages()
const page = pages[pages.length - 1]
data[Shortcuts.NodeName] = element.dataName
page.setData({
[toCamelCase(data.nn)]: data
})
// ...
}
},
}要点:这是 「内核 + 平台补丁」 模式:核心 runtime 保持通用,差异通过 hostConfig 合并。
6. 与「编译成各平台源码」的关系
严格说:
- JS:编译成小程序可加载的 JS 包(+ 可能的预构建、代码分割),不是「翻译成 WXML 里的 JS」。
- WXML/XML:由 模板引擎(shared + platform) 生成,与 hydrate 产出的 data 形状 协议对齐。
- 样式:见 05-CSS与样式转换.md。
参考资料
packages/shared/src/template.tspackages/taro-runtime/src/hydrate.ts、dom/root.ts- Taro 3 运行时相关官方博客(搜索「Taro 3」「运行时」)