运行时

service 跑逻辑树,
宿主跑渲染树

packages/runtime 是 Tiny 的小程序内核,不引用 window、document 或 iframe。service 线程用 glass-easel 维护逻辑树,负责 App/Page/Component、页面栈、setData、observer、relations 和模板求值;render 线程由宿主持有真实渲染树,只负责内置组件、样式、布局和 DOM。两棵树通过 tree.flow、tree.patch 和 tree.event 同步。

2 运行时树23 内置组件9 消息域5 组件生命周期
双侧树逻辑树在 service渲染树在 host不再产出 render.js

总览

两棵树,各管各的

service 执行小程序逻辑并维护 glass-easel 逻辑树;render 线程由宿主持有真实组件树。两棵树不共享内存,靠稳定的 node id、component flow 和 virtual node patch 对齐。

service thread                              render thread (one root per page)
+-------------------------------+           +-------------------------------+
| App / Page / Component        |           | host TreeRenderer             |
| glass-easel logic ComponentSpace |  tree  | real built-in components      |
| template -> virtual RenderNode | <------> | glass-easel render tree       |
| data / observers / relations  |  flow    | DOM / layout / styles         |
| node ids + patches            |  patch   | no app methods                |
+-------------------------------+           +-------------------------------+
service

逻辑树和业务状态

glass-easel ComponentSpace 在 service 中运行,自定义组件是完整实例,拥有 data、properties、observers、relations、lifetimes、事件回调、带类型的 dataset 和 node id。模板也在 service 求值,输出 RenderNode 虚拟树。

render

真实组件和 DOM

每个页面有一个 render root。宿主的 TreeRenderer 创建真实内置组件、自定义组件容器、slot、shadow root、样式和布局。render 不执行 app methods、observer 或 relations。

node id 由 service 分配

service 在创建虚拟节点时分配稳定 id,render 只维护 id 到真实组件的映射。后续 patch、事件、查询和元素树检查都以这个 id 为锚点,而不是从 DOM 反推小程序语义。

当前模板更新走 virtual node patch;component flow 只负责首屏挂载(define、create-node、props、children、ready)。增量不依赖微信旧的 FLOW_UPDATE/FLOW_SET_DATA/FLOW_DATA_OBSERVER,snapshot diff 已经覆盖文本、属性、结构和重排。

service 线程

service 线程:逻辑树和页面栈

service 是“小程序应用”真正运行的地方,由 bootServiceThread 一次性装配。

  1. 安装兼容全局

    按 runtimeCompatibility 写入 App/Page/Component、getApp/getCurrentPages 等全局;API 对象安装到配置的 namespace 和 aliases。未实现的 API 返回 undefined,并产生 UNSUPPORTED_API 警告。

    installMiniProgramGlobals(registry, apiHost)
  2. 注册 App

    service.js 被导入时执行 App(...),registerApp 会立即调用 onLaunch 和 onShow。此时页面还没有创建。

    registerApp(options)
  3. 整理 Page 和 Component 定义

    Page() 按顺序绑定到 manifest 里的页面路径;Component() 执行 normalizeComponentDefinition,合并 behaviors、observers、relations、options 和 externalClasses。

    registerComponent(options)
  4. bootstrap 创建逻辑树

    等待 loadArtifacts,加载 templates 和 styles,构建组件 schema,创建 LogicTreeAdapter,挂载首屏逻辑树并触发 onLoad/onShow。

    control('runtime', 'bootstrap')
  5. render 就绪后同步首屏

    宿主的渲染树挂载完成后发送 tree.ready,service 用逻辑树快照发送 tree.flow;随后宿主发 page.ready,service 触发 onReady。

    control('page', 'ready', { pageId })
页面操作service 逻辑树render 渲染树
page.navigateTo当前页 onHide;创建 pageId;挂载新页逻辑树;onLoad → onShow宿主创建新的 render root,应用新页 flow
page.navigateBack移除页面;倒序触发 onHide → onUnload;恢复页 onShow每页 render 模式保留原 root;单 render 模式重新挂载
page.redirectTo / reLaunch销毁旧页,创建目标页;执行对应生命周期关闭不再使用的 render root,挂载新页
page.switchTab复用 tab 页缓存,更新 tabBar 状态切换可见 root,关闭已卸载页
page.hide / page.show分发页面和组件 pageLifetimes只切换可见性,不销毁渲染树
page.unloadonUnload;组件 detached;清理逻辑树和 relations关闭 render root

页面实例和组件型页面

普通页面保留 pageId、path、query、data、methods 和生命周期记录。组件型页面(bodyType=component)使用整理后的组件定义:URL query 会按 property 类型转换,并执行 created/attached/ready/detached。

页面根节点也会进入逻辑树(page:<pageId>),所以 relations 和 selectComponent 能识别页面边界。

Taro 兼容仍然保留

普通页面会把顶层函数(包括生命周期)绑定到实例,兼容 Taro 读取 this.onReady.called 的写法;组件型页面走标准 Component 定义。

这是兼容路径,并不等于完整还原了微信的页面语义。

返回页面时 DOM 是否保留,取决于 render 模式

每页 render 模式下,每个页面有独立的 render iframe,返回时只需切换可见性,DOM、滚动位置和定时器都能保留。单 render 模式下所有页面共用一个 render 线程,返回时需要重新挂载页面。devtool 默认使用每页 render 模式,测试宿主默认使用单 render 模式。

命名边界

兼容输入可配,Tiny 内部名固定

runtimeCompatibility 只决定应用需要哪些兼容全局和 behavior scheme;逻辑树和渲染树的内部名称由 TINY_ENGINE_NAMING 统一提供。

// tiny.config.json
{
  "compatibility": {
    "api": { "global": "my", "aliases": ["wx"] },
    "globals": {
      "app": "HostApp",
      "page": "HostPage",
      "component": "HostComponent"
    },
    "behaviorScheme": "my://"
  }
}

// manifest.runtimeCompatibility
{
  "api": { "global": "my", "aliases": ["wx"] },
  "globals": { "app": "HostApp", "page": "HostPage", ... },
  "behaviorScheme": "my://",
  "opaqueGlobals": ["webpackJsonp", "cloud", "env"]
}

// 固定内部命名
{
  "hostTag": "tiny-add-button",
  "hostAttribute": "wx-host", // 临时同步 glass-easel
  "styleScopeAttribute": "wx-style-scope",
  "scopeMarkerClass": "tiny-scope",
  "tagClassPrefix": "tiny-tag-",
  "behaviorPrefix": "tiny-",
  "behaviorScheme": "tiny://"
}
service

兼容全局与 API

installMiniProgramGlobals() 按 runtimeCompatibility 安装配置的 App/Page/Component/Behavior/getApp/getCurrentPages,以及 API namespace 和 aliases。

canIUse()、诊断和建议信息也使用同一组 namespace。

render

Tiny 内部命名

逻辑树和渲染树使用 tiny-* host node;scope marker、tag marker 和内置 behavior 都从 TINY_ENGINE_NAMING 读取。

glass-easel 尚未公开 host/style scope 属性配置,因此 TINY_ENGINE_NAMING 暂时同步其 wx-host / wx-style-scope;runtime 不做 DOM 观察或属性改写。

名称输入侧配置当前内部值生效位置
模板指令wx:if / directive:if编译期消除TmplGroup directivePrefix
API 全局wx / my不参与内部命名installMiniProgramGlobals
组件宿主add-buttontiny-add-buttonComponentSpace hostNodeTagName
宿主属性—wx-host(临时)render tree / 样式编译器
style scope attribute—wx-style-scope(临时)glass-easel DOM
内置 behaviormy://component-exporttiny-base / tiny-itemruntime + web-components
tag / scope marker—tiny-tag-* / tiny-scope模板注入 + WXSS
输入兼容不等于内部命名

切换 API 或指令 namespace 不会改变 DOM、CSS scope 或内部 behavior。要支持第三方微信库时只增加 api.aliases: ["wx"]。当前唯一例外是 glass-easel 尚未配置化的 host/style scope 属性名。

数据同步

setData 走逻辑树,更新走 patch

页面和组件的 setData 只改 service 的逻辑树。模板重新求值后,service 把旧虚拟树和新虚拟树做 diff,再把 patch 发给渲染树。

  1. 写入逻辑树

    页面或组件调用 setData 时,先做 cloneJsonSafe,再按 a.b、list[0] 这样的路径写入 service 的 data。

    cloneJsonSafe(patch) · applyDataPatch(data, patch)
  2. 执行 observer

    service 侧的 observers 和 property observer 按变更路径匹配执行;property observer 会收到旧值。

    runObservers(instance, observers, changedPaths, oldData)
  3. 重新求值模板

    逻辑树里的 glass-easel 重新计算模板,得到新的虚拟 RenderNode。

    logicTree.updateRenderTree(pageId)
  4. diff 出 patch

    旧快照和新快照按 text、replace、props、insert、remove、reorder、extra 做 diff,同一个 microtask 内的更新会合批。

    diffRenderTree(previous, next)
  5. 渲染树应用 patch

    宿主 TreeRenderer 把 patch 应用到真实组件和 DOM;service 不等待 render 的确认。

    tree.patch
Component({
  properties: {
    value: { type: String, value: '', observer: 'onValue' },
  },
  observers: {
    value(value) { this.setData({ derived: value.toUpperCase() }) },
    'a, b'(a, b) { this.setData({ sum: a + b }) },
    '**'(data) { /* 任意变化 */ },
  },
  methods: {
    onValue(value, oldValue) {
      this.setData({ changed: value !== oldValue })
    },
  },
})

没有 render 侧 data 镜像

组件 data、observer 和 property 都只在 service 逻辑树里。render 只收到节点 props 和 patch,不保存第二份小程序 data。

observer 只在 service 执行

render 发现 DOM 事件后把 node id 和 native 字段发回 service;事件处理、setData、observer 和模板求值都在逻辑树完成。

递归和异常都有诊断

observer 递归深度上限是 20,超过会产生 OBSERVER_RECURSION_LIMIT;observer 抛错会产生 OBSERVER_ERROR,不会影响其他逻辑。

事件

事件从渲染树回到逻辑树

渲染树只负责把真实 DOM 事件转成 tree.event:带 node id、事件名、detail、native 字段和 touch 列表。service 用 node id 找到逻辑节点,取出带类型的 dataset,再走 glass-easel 的捕获/冒泡。

  1. 渲染树收到原生事件

    宿主内置组件和 DOM 监听器先处理点击、输入、滚动、触摸等原生事件,并归一化 detail。

    host event listener
  2. 发送 tree.event

    渲染树按稳定 node id 发出事件;不再传 componentData,也不要求 service 根据 tag 猜事件语义。

    sendEvent('tree', 'event', payload)
  3. service 找到逻辑节点

    逻辑树按 node id 找到绑定监听器的元素,读取 typed dataset 和 id,执行 capture/bubble。

    logicTree.dispatchViewEvent(payload)
  4. 调用页面或组件方法

    逻辑树调用对应的方法;抛错会转成 EVENT_HANDLER_ERROR 诊断。

    page.invokeMethod(handler, [event])
事件对象

service 收到的内容

{
  viewId: 'node-42',
  name: 'tap',
  detail: { ... },
  native: { clientX: 120, clientY: 80, ... },
  touches: [ ... ],
  changedTouches: [ ... ]
}
handler 对不上也会出诊断

EVENT_HANDLER_MISSING 和 EVENT_PAGE_NOT_FOUND 都是 warning,不会影响其他事件。

组件事件

triggerEvent 不再跨线程

自定义组件的 triggerEvent 在 service 的逻辑树内部完成捕获和冒泡,不会为了一个自定义事件再走一次 render 往返。

内置组件

detail 由宿主归一化

button、input、scroll-view、swiper、canvas 等内置组件把原生字段归一化成小程序 detail;service 只负责分发,不再按 tag 合成领域事件。

组件语义

组件定义、observer、relation 都在逻辑树

service 侧维护完整的组件定义、逻辑树和关系图。render 只保留组件容器和真实节点。

能力实现关键行为
BehaviornormalizeComponentDefinition嵌套 behaviors 去重并检测循环;执行顺序是被引用 behavior → 引用者 → 组件;组件覆盖 properties/methods/data
component-exportwx://component-export(scheme 可配置)+ export()selectComponent 返回 export() 的结果,而不是组件实例
observersobservers / property observer支持对象/数组写法、逗号分隔的多路径、** 和 .** 通配;property observer 收到旧值;递归上限 20
relationsRelationManager + ComponentTreeparent / child / ancestor / descendant;双方都声明才会建立;支持 linked / unlinked / linkChanged / linkFailed
选择器selectComponent / selectAllComponents / selectOwnerComponent默认不穿透组件边界;>>> 可以显式穿透;结果按逻辑树顺序返回
组件型页面MiniProgramPageInstance + 整理后的定义query 按 property 类型转换;执行 created/attached/ready/detached;页面根也会进入逻辑树
behaviors

合并顺序和限制

每个 behavior 的 properties/data/methods/lifetimes/pageLifetimes/observers/relations/externalClasses/options 都会被收集;重复引用只执行一次。

内置 behavior id(默认 wx://...)目前只支持 component-export,其他会报 UNSUPPORTED_BEHAVIOR。Behavior 的 definitionFilter 已按引用链执行;写在 Component 上的 definitionFilter 仍会报诊断。

relations

双方声明才会建立关系

父组件声明 child 时,子组件也要声明对应的 parent;祖先和后代同理。只声明一侧不会连上。

attached 触发 linked,detached 触发 unlinked,移动时触发 linkChanged;父级或祖先找不到目标时触发 linkFailed。组件型页面的 relations 会按 page shadow tree 的 target path 匹配,并手动触发两端的 linked/unlinked。

tree

逻辑树和渲染树只共享 node id

逻辑树保存 pageId、path、tag、id、class、parent、顺序和虚拟节点;渲染树只保存 node id 到真实组件的映射,不保存组件 data 或 observer。

页面生命周期

按实例分发

page.show/hide 会遍历 service 里的所有组件实例,逐个调用 pageLifetimes。render 没有 componentPageLifetime 回路,避免重复执行。

generics、definitionFilter 和 pureDataPattern 已接入;componentPlaceholder 仍未实现。

样式

组件配置进入 flow

multipleSlots、virtualHost、styleIsolation、addGlobalClass 和 externalClasses 会随 component schema/flow 下发给渲染树,渲染树用它们创建组件容器和作用域。

渲染树

渲染树:宿主实现真实组件

渲染树由宿主实现。runtime 只定义 TreeRenderer 契约:mount、applyPatches、applyStyles、query、measure、inspectTree、observeIntersection、dispose。它不知道 WXML,也不执行 app methods。

export type TreeRenderer = {
  mount(root: RenderNode): void
  applyPatches(patches: RenderPatch[]): void
  applyStyles?(styles: string[]): void
  setEventSink?(sink: (event: TreeEventPayload) => void): void
  setPagePath?(pagePath: string): void
  query?(selector: string, all: boolean): unknown[]
  measure?(ids: string[]): Record<string, unknown>
  inspectTree?(): unknown
  observeIntersection?(request, emit): (() => void) | undefined
  dispose?(): void
}
Web

createGlassRenderTree

@tiny/host-web-components 的 Web 渲染树注册真实 tiny-* 内置组件,维护 slot、shadow root、class/scope、样式和 DOM;createGlassRenderEntry 负责创建它并安装基础 CSS。

Native

createNativeTreeRenderer

同一个 TreeRenderer 契约,把 RenderNode 和 patch 映射到原生视图。目前只有契约和 headless smoke test,没有实际原生宿主。

Test

headless renderer

runtime 单测用 headless renderer 验证整棵树、patch 顺序、node id 和事件回传,不需要 iframe 或 DOM。

首屏:tree.flow

service 把组件定义、create-node、props、children 和 ready 编成 component flow,渲染树按顺序创建真实节点,最后把页面根挂到容器。

更新:tree.patch

service 对虚拟树做 diff,渲染树只应用 text、replace、props、insert、remove、reorder、extra。patch 按 pageId 合批,保持页面内顺序。

元素树

inspectTree

渲染树按自己的真实组件树返回 InspectNode,包括 tag、attributes、className、style 和 children;宿主和 devtool 通过 runtime.inspectTree 读取。

  • 最多 2000 个节点
  • 最大深度 60
  • 文本截断到 200 个字符
render 不负责业务路由

渲染树不保存页面栈,也不决定页面切换。它只响应宿主转发的 tree flow/patch、query/canvas 控制和 inspectTree,并把事件按 node id 回传 service。

内置组件

23 个内置组件由宿主提供

内置组件不再是 service 里的玻璃组件,也不是 render 里重新求值的 WXML;service 只输出语义 tag,宿主把它们映射成真实内置组件。

AOT

模板和样式

build-templates.mjs 把 23 个 template.wxml 编成一个模板 group;build-styles.mjs 合成基础样式加 23 份组件样式。

生成物在 hosts/web-components/src/generated,不提交到仓库。

host-owned

Web 内置组件

@tiny/host-web-components 用真实 tiny-* 自定义元素实现 view/button/input/textarea/picker 等组件,并维护 behaviors、默认 CSS、事件归一化和 canvas surface。

semantic

service 只发语义 tag

逻辑树里的内置节点是语义虚拟节点,不带 Web/native 实现。无论宿主是 Web 还是原生,service 发出的是 view、button、checkbox-group 这类小程序 tag。

组件组当前支持实现方式
view / text / label / image / iconhover、inline、selectable、decode、space、aria;label for;图片 mode/background真实 tiny-* 宿主 + slot;默认 CSS
buttontype/size/plain/loading/disabled、formType、openType 事件映射、hover 时序tiny-button + 宿主 tap;form/hover behaviors
input / textarea / pickervalue、focus、confirm、selection、composition;picker 列和值原生输入 / 自定义 picker surface
checkbox / radio / switch / sliderchecked/disabled/value/color;change/checkboxstate;group change自定义 22×22 等结构 + 隐藏原生输入
scroll-view / swiperscrollX/Y、scrollTop/Left、refresher、drag/scroll;swiper change/transitionCSS overflow / transform + touch/scroll 监听
progress / canvaspercent/showInfo/active;canvas-id、2D/WebGL、rAF、createImage、touchtiny-progress;canvas surface + OffscreenCanvas handoff
仍然不是完整组件库

navigator、video、rich-text、map、movable-view 等还没有实现;已有组件的 open-type、表单提交、无障碍和 Skyline 语义也不完整。

样式隔离

样式:service 算 scope,render 应用

样式表按 cascade role 分类(builtin / app / page / component / page-scope / shared)。service 过滤并计算 scope,渲染树把样式应用到真实组件树;内置 CSS 始终归宿主。

配置生效范围行为
页面 isolation页面根 + 子组件默认 shared;显式 isolated/apply-shared/shared 优先
组件 isolated组件自身默认值;不接收页面样式
apply-shared组件自身页面样式可以影响组件;组件样式不影响页面
shared组件自身 + 页面组件样式按页面 scope 重新生成一份
addGlobalClass组件自身等价 apply-shared;可由 JSON 或 options 设置
externalClasses组件根节点把外部 class 透传到组件内部

逻辑树负责

  • 读取 manifest.styles 的 role 和 scope
  • 注册 glass-easel 逻辑树样式
  • 在 tree.flow 里带页面/组件样式文本和 fallback app 样式

渲染树负责

  • 应用 flow 里的样式
  • 维护真实组件的 scope 和 class marker
  • 用样式作用域渲染内置组件和自定义组件容器
低优先级 CSS 还没有启用

CLI 仍会写 *.low.css,但目前没有启用 convertHost,runtime 也不会加载它。:host 组合和优先级还没有系统整理。

Canvas 与查询

Canvas、查询和相交观测

canvas 已经从核心 runtime 移出。service 通过 ServiceCanvasAdapter 取得 OffscreenCanvas 控制面,渲染树提供画布 surface 和测量;selector query 和 IntersectionObserver 通过 query 控制面回到真实 DOM。

Canvas handoff

2D / WebGL / RAF

浏览器宿主把 render iframe 的 OffscreenCanvas 通过 postMessage 中继给 service;之后 service 直接持有控制面,支持 2D、WebGL、requestAnimationFrame 和 createImage。不再走 PNG 帧镜像。

SelectorQuery

在渲染树里查节点

createSelectorQuery 把 selector、page/component 作用域和 all 发给渲染树;宿主返回 rect、dataset 和 scroll。.node() 仍走 canvas handoff,并支持组件作用域下的样式 class。

IntersectionObserver

observe / unobserve

service 把 observe 请求和 view id 发给渲染树;Web 宿主用原生 IntersectionObserver,entry 通过 query.intersection 事件回到逻辑树。

animation

仍是最终态 CSS

wx.createAnimation 会生成链式的 CSS transform/transition;编译期把 animation 属性改写成内联 style。多 step 时序和事件回调还没有还原。

限制

当前限制

两侧树已经接管默认宿主,但下面这些仍是明确缺口。

事件

tree.event 还没有 target geometry

事件只带 node id、name、detail、native 字段和 touches;目标节点的几何信息由 service 侧逻辑节点补足,和微信协议仍有差别。

原生

原生渲染器只有契约

createNativeTreeRenderer 有契约和 headless smoke test,没有实际原生宿主。原生宿主还需要实现 OffscreenCanvas/查询/相交观测等 host 能力。

组件

componentPlaceholder 等未实现

componentPlaceholder、trait behaviors / chaining API 和大多数微信内置 behavior 仍未实现;generics、definitionFilter、pureDataPattern 已接入。

页面

页面形态不完整

14 项页面生命周期和 5 个路由 API 已接,分享回调、下拉刷新和退出状态可用;分享菜单 UI、页面栈恢复、页面转场和自定义 tabBar 还缺。

API

API 覆盖有限

目前支持 storage、导航、system、toast、animation、canvas 和 selector query;request、媒体、文件、账号、位置、WebSocket 还没有。

继续阅读

这些问题在宿主侧怎么处理,可以看 宿主适配;现有测试覆盖到哪里,可以看 测试与验收。