跨端框架与原生组件库,我把两套小程序方案的源码都拆了
说明:本文分析对象为两个开源项目,出于合规考虑,文中以「跨端框架 A」(一套代码编译到多端的框架)、「原生组件库 B」(某小程序平台官方 UI 组件库)等代称指代,代码均为重写后的简化伪代码,仅用于说明设计思路,与真实源码存在差异。
引言:同一个平台,两种截然不同的活法
小程序开发圈有一个长期争论:到底该用跨端框架,还是老老实实写原生?
争论双方往往各执一词:
- 框架派说:一套代码跑 N 个端,开发效率碾压;
- 原生派说:跨端框架是"胶水代码",性能差、包体大、踩坑无解。
但这些说法大多停留在使用层。这篇文章做一件更彻底的事:把一个跨端框架和一个官方原生组件库的源码都拆开,从编译机制、运行时架构、组件系统、样式处理、生命周期五个维度逐项对照,看两套方案在同一批问题上分别给出了什么答案。
先说两个主角的定位:
- 跨端框架 A:双层架构——编译时把 React/Vue 代码转译为各端产物,运行时在小程序环境里重建虚拟 DOM 和事件系统。Monorepo 里有 60+ 个包:编译器(webpack5/vite 双 runner)、运行时(虚拟 DOM + setData 调度)、十几个平台包(各小程序平台 + H5 + 鸿蒙 + RN)、每个 UI 框架一个桥接包。
- 原生组件库 B:某小程序平台官方出品,约 20 个基础组件。没有编译到别端的层,没有运行时抽象层——组件就是原生构造器 + 模板 + 样式三件套,构建链只负责把 TypeScript 编译回
.js、把 less 编译回.视图层脚本s。
一句话立论:框架 A 是"用编译时复杂度换运行时兼容性",组件库 B 是"用平台绑定换确定性"。这两个极端恰好构成跨端技术光谱的两端。
读完你应该能拿到两样东西:
- 13 个可以单独拎出来讲的源码级技术难点——每个都附"面试话术"版本;
- 一个判断标准:下次选型时,你能说清楚自己到底在为什么买单。
一、跨端编译机制:转译的艺术 vs "不存在"的编译层

1.1 框架 A:三层编译链路
框架 A 的编译链路是 CLI → 编译服务 → 构建器:
源码 (React/Vue)
│
├─► 构建器(webpack5-runner 或 vite-runner)
│ │
│ ├─ 处理框架本身的编译(JSX/Vue SFC → JS)
│ │
│ └─ 平台插件:生成模板、替换组件库
│
└─► AST 级转换器(旧编译模式补充)
还原 TS class property、提升 JSX 闭包、处理 视图层脚本
三个值得写的设计点:
(1)编译器与平台的插件式解耦。 构建器的核心插件不认识任何具体平台——它只认两个接口:template(平台模板对象)和 fileType(文件后缀约定)。各平台包负责提供自己的模板实现和语法适配器。这就是为什么这个框架能同时支持十几个小程序平台 + H5 + 鸿蒙 + React Native:每新增一个平台,编译内核一行不改。
(2)双模板策略:递归 vs 逐层展开。 这是整个编译层最精彩的分歧点。框架的运行时把页面渲染成一棵虚拟 DOM 树,最终要生成模板来"递归地"渲染任意深度的树。问题是:
- 有的平台模板支持递归(可以调用自身),那一份模板就够了;
- 有的平台不支持模板自调用(比如微信),怎么办?
解法是把"递归"在编译期展开为"逐层":基类提供 16 层嵌套的展开能力,构建时按 baseLevel 生成 16 份结构相同、层次递进的模板;同时做优化裁剪——扫描代码里每个组件的实际嵌套深度,只保留用得到的层数,模板体积从最大 16 层裁到实际需要的层数。
这是"运行时能力差异 → 编译产物差异"的教科书案例:平台缺什么,编译期就补什么,运行时一份代码不用改。
(3)H5 端根本不走模板。 生成 wxml 那套逻辑在 H5 平台完全旁路——H5 平台包的做法是用构建别名,把统一组件包整体替换为浏览器 DOM 组件库。同一份业务代码,小程序端走"模板 + 数据",H5 端走"真组件树",两条路径在平台包层面分流。
// 简化伪代码:平台包注入模板,编译内核不认识具体平台
class MiniPlugin {
constructor(options) {
// 只依赖抽象接口:template / fileType
this.template = options.template // 由平台包提供
this.fileType = options.fileType
}
// 编译产物 = 框架 JS + 平台模板 + 组件注册
}
1.2 组件库 B:编译层"不存在",但构建链有讲究
组件库 B 没有任何"编译到别端"的概念。它的构建链只做一件事:把开发者友好的源码形态,编译回小程序原生的三件套(.ts → .js、.less → .视图层脚本s、模板/配置原样输出)。
但"没有编译层"不等于构建没有技术含量,两个细节:
(1)单文件产物的硬约束。 组件的 JS 用打包器编译,但强制配置 maxChunks: 1——每个组件必须产出一个自包含的单文件,禁止代码分块。为什么?因为小程序的组件是按目录整体被引用的,业务方通过"构建 npm"机制消费这个目录,任何异步分包或 chunk 拆分都会破坏原生引用协议。这是"宿主平台的消费方式反向约束构建配置"的例子。
(2)依赖图驱动的构建。 构建前先用脚本从组件清单入口递归收集组件依赖图(每个组件用到了哪些其他组件的 js/模板/样式/配置文件),按依赖图决定哪些文件进发布产物,并做增量编译跳过。
1.3 面试话术小结
一句话版:
“框架 A 的编译层是插件式的:构建内核只认模板和文件类型两个抽象接口,各平台包注入自己的模板实现;微信端模板不支持递归,就用编译期逐层展开 + 嵌套深度裁剪。组件库 B 没有编译层,构建链只负责把 TS/less 编译回原生格式,并用单文件约束适配构建 npm 协议。”
深入版:
“编译内核与平台解耦的关键是把’渲染任意深度虚拟树’的需求翻译成模板对象接口。支持递归的平台一套模板搞定;不支持的平台在编译期做 16 层逐层展开,再扫描真实嵌套深度裁剪层数压体积。H5 端则完全旁路模板,用构建别名把统一组件包替换成 DOM 组件库。原生组件库的构建反向被宿主消费协议约束——组件产物必须单文件自包含,所以打包器强制 maxChunks=1。”
二、运行时架构:重建一个 DOM vs 什么都不建
这是两套方案差异最大的地方,也是跨端框架"性能税"的真正来源。
2.1 框架 A 的运行时:在小程序里造一个浏览器
小程序环境没有 DOM。但 React/Vue 需要操作 DOM。框架 A 的答案是运行时自建一套迷你 DOM,由四块组成:
┌───────────────────────────────────────┐
│ 运行时(小程序端) │
│ │
│ 虚拟节点树 事件系统 │
│ ├ Node(_path 路径) ├ 事件中心(可替换)│
│ ├ Element(style 等) └ 合成事件对象 │
│ └ RootElement(调度) │
│ │
│ hydrate(树 → 平台数据) 页面/组件配置 │
└───────────────────────────────────────┘
│ 路径式 setData(增量)
▼
小程序原生渲染层
(1)路径式增量 setData——整个运行时的灵魂。
每个虚拟节点在插入树时计算一个路径:父节点路径 + '.cn.' + 下标。这样树上任意一个节点的变更,都能翻译成一个精确的更新路径,只把这个路径下的数据发给渲染层:
// 简化伪代码:路径式增量更新
node._path = parent._path + '.cn.' + index
// 只 setData 变化的分支,而不是整棵树
setData({ 'root.cn.3.cn.0.st': 'color:red;' })
配套的调度细节有三个,全部来自真实源码:
- 路径去重:如果一次更新里既有父数组整体重置、又有其子项更新,就删掉子路径,用一次大更新覆盖,避免同一数据被 setData 两遍;
- 批量队列:同一轮的多次变更合并成一个更新负载,而不是逐个 setData;
- 用
setTimeout而不是微任务调度——源码里明确注释了原因:小程序一次渲染流程内可能连续触发更新,用微任务会在同一次渲染里发两次 setData,宏任务才能对齐渲染节奏。"为什么不用 Promise.then"是面试官最爱追的点,答案藏在源码注释里。
(2)局部更新组件:手工实现"组件级重渲染隔离"。
上述路径式更新仍以页面为单位通信。当局部更新频繁时,框架提供一个编译层组件作为"更新容器":运行时在树上查找变更节点最近的容器节点,把 setData 范围从页面级缩小到该组件级,并缓存查找结果。这个组件在组件库里是个空壳——<Host/> 里什么都没有,它的全部语义存在于编译产物和运行时调度里。这等于在没有组件级重渲染隔离的渲染模型上,手工造了一个。
(3)在无 DOM 的环境里重建事件冒泡。
原生事件先到框架的事件中心,被包装成合成事件对象(target/currentTarget 用 getter 惰性求值),再根据事件绑定时记录的节点标识反查节点,模拟出捕获-冒泡-委托的完整语义。React 开发者写的 e.target.dataset 能正常工作,靠的就是这层模拟。
(4)全局单例 + 钩子系统。
- 一个 25 行的全局单例对象
{ app, router, page }持有当前上下文,配合getCurrentInstance()API——设计简单,但依赖"同一时刻只有一个页面在渲染"这个时序前提; - 运行时零 import React/Vue。事件中心、生命周期实现、元素补丁全部通过
hooks.call(...)钩子注入,由各 UI 框架的桥接包提供实现。这是"运行时不绑定任何框架"的关键设计。
2.2 组件库 B 的运行时:不存在,但复杂逻辑去哪了?
组件库 B 没有 VDOM、没有 diff、没有响应式包装。但这不代表它回避了框架 A 用运行时解决的那些问题——它用了完全不同的手段:
(1)手势动画下沉到视图层脚本。 滑动菜单组件(左滑出现操作按钮)的手势逻辑写在视图层脚本(视图层脚本)里,直接在渲染线程响应触摸事件,通过桥接方法把结果传回逻辑层再触发自定义事件。对比框架 A 的"逻辑层模拟事件冒泡"——同一个"交互性能"问题,一个在逻辑层重建事件模型,一个把逻辑推到渲染线程,是两条路线最锋利的对照点。
(2)唯一的"运行时"是一个几十行的主题订阅器。 应用入口维护一个监听器数组,页面在加载时注册回调、卸载时注销,主题切换时遍历通知。这是整个组件库里唯一称得上"全局运行时"的东西。
(3)零抽象的收益直接体现在产物上:每个组件的 JS 就是原生构造器调用,没有任何框架运行时代码被打进产物,包体和启动开销都接近手写原生。
2.3 面试话术小结
一句话版:
“框架 A 在小程序里自建迷你 DOM:节点路径支撑增量 setData,路径去重加批量队列加 setTimeout 调度控制通信频率,局部更新组件把通信范围缩到组件级,并用钩子系统做到运行时零依赖具体框架。组件库 B 零运行时,手势逻辑下沉视图层脚本,唯一全局逻辑是主题订阅器。”
深入版:
“增量 setData 的基础是节点路径 = 父路径拼接下标,任意节点变更都能翻译成精确更新路径。调度上三个细节:父子路径去重避免重复覆盖、更新负载批量合并、用 setTimeout 而非微任务对齐渲染节奏——微任务会导致一次渲染内发两次 setData。通信范围上,运行时查找变更节点最近的局部更新容器做组件级 setData 并缓存查找结果,该组件本身是空壳,语义全在调度层。框架解耦靠钩子:运行时通过 hooks.call 注入生命周期、事件中心,自身不 import React/Vue。原生路线则是把交互逻辑用视图层脚本跑在渲染线程,绕开跨线程通信。”
三、组件系统:桥接三个框架 vs 用尽构造器特性
3.1 框架 A:一个组件库,四个"皮"
核心组件库用 Web Components 风格的编译器(Stencil)定义一份,然后每个 UI 框架一个桥接包,把这一份组件"穿"到 React/Vue3/Solid 上。H5 端还有一套"是否用 HTML 语义标签"的开关,对应两个组件库变体。
桥接层怎么工作?Vue3 的实现最值得写:
// 简化伪代码:把 Vue 应用"挂"到小程序页面上
function createVue3App(appComponent) {
const pages = []
// 重写根组件的 render:渲染结果 = 所有活跃页面组成的数组
rootComponent.render = () => pages.slice()
// 每个小程序页面 onLoad 时:
// 1. createPage(component) 创建该页面的 VNode
// 2. pages.push(vnode) 塞进根的渲染结果
// 3. root.$forceUpdate() 强制根组件重渲染
}
核心思想:一个 Vue 应用实例管理 N 个小程序页面,每个页面是根组件 render 数组里的一个 VNode。 React 端思路类似但用自定义渲染器实现。另外应用的全局数据通过 defineProperty 代理到全局 app 对象上,保证 getApp() 语义可用。
组件树到原生组件的映射:运行时把虚拟树序列化为平台数据,递归渲染组件声明为带两个 property 的原生组件,接收子树数据自行渲染(即编译层那个"任意节点渲染器");virtualHost 保证它不产生多余节点。
3.2 组件库 B:原生构造器的三个进阶用法
组件库 B 每个组件四件套:.ts(构造器逻辑)+ 模板 + .less + .json。基础用法不展开,讲三个官方文档都不太提的进阶设计:
(1)relations 命令式直连。 表单组件和子表单项(输入格、勾选组)之间需要通信:表单要收集所有子项做统一校验。它没用事件冒泡,而是用组件关系声明:
// 简化伪代码:表单通过关系声明直接持有子组件实例
Component({
relations: {
'../cell/cell': { type: 'descendant',
linked(target) {
// 直接持有子组件实例,直接调它的方法
this.formItems[target.data.prop] = target
target.setInForm()
},
unlinked(target) { delete this.formItems[target.data.prop] }
}
}
})
父组件在 linked 回调里直接持有子组件实例引用、直接调用子组件方法——一条"命令式直连"通道,绕过整个事件系统。跨层级表单校验聚合就是这么实现的。
(2)样式扩展属性替代外部样式类。 组件样式隔离下,外部 class 默认进不了组件内部。标准解法是"外部样式类"机制,但这个库全仓库没用它,而是在每个组件上定义一组样式扩展属性(extClass、bodyClass、footerClass……),作为普通 property 传进来,拼进模板的 class 字符串:
<!-- 简化伪代码:外部 class 走普通 property -->
<view class="xx-cell {{extClass}}">
<view class="xx-cell__bd {{bodyClass}}">...</view>
</view>
好处是扩展点显式可控:组件明确声明哪些位置可定制,而不是把整个样式面都暴露出去。
(3)type: null 属性传函数。 属性系统默认不允许 function 类型的值。但上传组件的 upload、搜索组件的 search 都是 type: null 的属性——专门用来传异步函数,组件内部调用它拿到 Promise:
// 简化伪代码:属性传函数,异步逻辑完全交给使用者
Component({
properties: {
upload: { type: null }, // 使用者传入 (file) => Promise<url>
},
methods: {
async doUpload(file) {
const url = await this.data.upload(file)
this.triggerEvent('success', { url })
}
}
})
这是属性系统的一个隐藏能力:组件只定义流程骨架,异步业务完全外包给使用者——比 slot 方案或回调注册方案都简洁。
3.3 面试话术小结
一句话版:
“框架 A 一份组件库配多个框架桥接包:Vue3 桥接把所有页面塞进根组件 render 的数组,一个应用实例管理 N 个页面。组件库 B 用原生构造器的进阶特性:relations 直接持有子实例做命令式通信、样式扩展属性替代外部样式类、type null 属性传异步函数。”
深入版:
“桥接的核心是重写根组件 render 为渲染 pages 数组,页面 onLoad 时创建 VNode 入数组并强制根更新——Vue 的响应式系统照常工作,只是根的子树换成了’页面集合’。原生侧三个黑科技:relations 的 linked 回调拿到子组件实例引用直接调方法,实现跨层级表单聚合;extClass 系列把外部 class 作为普通 property 显式声明可定制点,比外部样式类可控;type:null 绕开属性类型限制传函数,组件定义流程骨架、使用者注入异步实现,本质是依赖注入。”
四、样式处理:转译 + 防御 vs 一份 CSS 适配双渲染引擎
4.1 框架 A:多端样式的"转译层 + 防御层"
样式跨端的本质问题是:不同端的 CSS 方言不互通——小程序用 rpx,H5 用 rem/vw,RN 是 JS 对象且只支持子集。
(1)转译层:按设计稿宽度统一换算。 构建链上的样式后处理插件维护一张"设计稿宽度 → 换算比例"表(375、640、750、828 各有系数),构建时按目标端把 px 转为对应单位:H5 转 rem 或 vw、小程序转 rpx、RN 保持 px、鸿蒙用特殊单位。开发者只按一份设计稿写 px,端差异全部在后处理里消化。还支持注释局部禁用转换(postcss-pxtransform disable)。
(2)防御层:为 RN 量身定制的容错。 CSS 转 RN 样式对象的转换器里全是防御性逻辑:
- 单位白名单正则——不在名单里的单位直接过滤,不让非法值进 RN;
- 非法 line-height 静默忽略——无单位或不支持的取值如果照转,RN 运行时会直接 crash,所以宁可丢一条样式也不崩 app;
- 需要保持物理像素的值,把
px转成大写PX绕过平台的自动缩放; - border 简写展开回填。
(3)静态拦截:stylelint 规则集。 再加一层针对 RN 的 lint 规则(未知属性、不支持的字重、必须有单位的行高),把问题拦在编码期而不是运行期。
三层合起来是一个完整的"转译 + 防御 + 静态拦截"体系——跨端样式不只是单位换算,更是对目标端容错能力的系统性补齐。
4.2 组件库 B:patch.less——全网稀缺的素材
组件库 B 不跨端,但要对标一个更冷门的难题:同一平台内两套渲染引擎的兼容。小程序除了传统 WebView 渲染,新出了渲染引擎(下称"新渲染引擎"),而它的 CSS 支持是子集:不支持伪类序选择器、不支持 em 单位、不支持无单位行高、不支持属性选择器。
组件库的解法是一个补丁样式文件,构建期做样式降级展开:
构建前(传统引擎可用,新渲染引擎不可用):
[class^="xx-icon-"] { ... } ← 属性选择器
构建后(双引擎都可用):
.xx-icon-success { ... } ← 展开为逐个 class
.xx-icon-waiting { ... }
.xx-icon-info { ... }
...(上百个)
em 单位 → 逐条换算为 px
无单位 line-height → 补单位
这是"渲染引擎碎片化下的 CSS 降级策略":不写两份样式,而是维护一份主样式 + 一份编译期补丁,把新引擎不支持的选择器/单位预编译展开成它支持的形式。同一平台的碎片化和跨端碎片化,解法竟然同构——都是"编译期消化能力差异",这个观察本身就能撑起一节内容。
主题方面是三层架构:
- 静态层:声明
darkmode: true,基础样式全部走 CSS 变量(--xx-FG-*系列),系统切深色模式时变量自动切换——零 JS 参与; - 动态层:应用入口监听系统主题变化,维护观察者数组;
- 混入层:一个几行的 Behavior + 一个包装
Page()的辅助函数,页面加载时自动注册回调、卸载时注销,把theme写进页面 data 供模板条件渲染。
CSS 变量管样式、JS 通道管结构,两条通道各司其职——比"全靠 JS 切换 class"或"全靠媒体查询"都干净。
4.3 面试话术小结
一句话版:
“框架 A 的跨端样式是转译+防御+静态拦截三层:后处理按设计稿宽度表分端换算单位,转 RN 时用单位白名单和非法行高静默忽略做容错,再用专属 lint 规则静态拦截。组件库 B 不跨端但要兼容同平台双渲染引擎,用补丁样式文件把新引擎不支持的选择器和单位编译期展开降级。”
深入版:
“单位换算的关键是 deviceRatio 表收敛多端:开发者按一份设计稿写 px,h5 转 rem/vw、小程序转 rpx、rn 保 px,注释可局部禁用。RN 防御的精髓是’宁可丢样式不可崩 app’:非法 line-height 静默忽略、px 转 PX 绕过缩放、单位白名单过滤。原生侧的 新引擎兼容补丁 做属性选择器到 class 的预编译展开和 em 到 px 换算,本质和跨端转译同构——都是编译期消化渲染能力差异。深浅色主题三层:CSS 变量随系统自动切换管样式,JS 观察者通道管结构,Behavior 混入自动订阅退订。”
五、生命周期管理:映射桥 vs 原生裸用
5.1 框架 A:三层生命周期映射
跨端框架的生命周期要同时满足三个体系:UI 框架生命周期(React 的 componentDidMount / Vue 的 onMounted)、小程序页面生命周期(onLoad/onShow/onReady/onHide)、小程序应用生命周期(onLaunch/onShow/onHide)。框架 A 的处理分三层:
(1)挂载时序:onLoad 里 mount,onReady 前完成首屏 setData。 页面配置在 onLoad 时挂载框架组件,并主动触发一次全量更新 + 用一个 Promise 标记保证首屏数据在 onReady 之前送达渲染层——否则会出现"页面 ready 了但内容是空的",这是跨端框架最经典的坑之一,源码里专门做了保障。
// 简化伪代码:挂载与首屏保障
Page({
onLoad() {
this.pageElement = mount(component, $pagePath) // 挂载框架组件
this.pageElement.performUpdate(true) // 强制首屏更新
},
onReady() {
this.hasLoaded.resolve() // 首屏数据已送达
}
})
(2)按需注册副作用生命周期。 onShareAppMessage 这类生命周期,只有业务代码真的声明了才注册进页面配置——避免无谓的配置膨胀。
(3)框架生命周期的"横向注入"。 以 Vue3 为例:小程序应用生命周期通过钩子系统拿到实现([ONLAUNCH, ONSHOW, ONHIDE]),映射为应用对象的属性描述符,再把组件选项里的 onLaunch/onShow/onHide 从 Vue 实例选项中取出调用。一句话概括这套设计的边界划分:UI 框架的生命周期归 UI 框架,小程序的生命周期由钩子系统横向注入,两套体系互不侵入。
5.2 组件库 B:直接用,还有"风格债"
组件库 B 没有映射问题——直接用原生生命周期。但源码里有两个可以写成"官方库也有风格债"的细节:
- 两种声明写法混用:有的组件用顶层
attached()/ready(),有的用lifetimes: { attached() {} }对象写法,全库不统一; - 用的是旧版属性级 observer 字段,而不是新的
observers对象(如对话框的show属性 observer、上传组件的files属性 observer)。
这不是"错",但说明即使是官方库,在 API 演进面前也会留下历史包袱——项目周期长于 API 稳定期时,风格漂移几乎不可避免。这个观察对读者很有共鸣感。
事件输出统一走 triggerEvent,命名规范是全小写单词拼接(buttontap、navigateerror、selectresult)。
5.3 面试话术小结
一句话版:
“框架 A 三层处理生命周期:onLoad 时挂载框架组件并强制首屏更新保证 onReady 前数据送达,副作用生命周期按需注册,应用生命周期通过钩子系统横向注入、不侵入框架自身生命周期。组件库 B 直接用原生生命周期,但存在顶层写法与 lifetimes 对象混用、旧版 observer 的风格债。”
六、性能代价的定量视角:跨端框架到底"贵"在哪
写对比文不谈性能就是耍流氓。把上面的分析收敛成一张"成本账单":
| 成本项 | 框架 A | 组件库 B |
|---|---|---|
| 运行时代码体积 | 虚拟 DOM + 事件系统 + 调度器全部进包 | ≈ 0,仅一个几十行主题订阅器 |
| 首屏链路 | 框架挂载 → 树构建 → 序列化 → setData → 模板渲染 | 构造器直接初始化,一次 setData |
| 更新通信 | 路径式增量 setData(有优化上限) | 原生数据绑定(平台原生优化) |
| 事件链路 | 渲染层 → 逻辑层包装 → 模拟冒泡 → 框架处理 | 原生事件直达(交互逻辑可下沉视图层) |
| 模板体积 | 编译期展开 + 裁剪后仍有多层模板 | 每组件一份手写模板,最简 |
但要强调两点公平性:
- 框架 A 的这些成本换来的是十几个平台 + 三个 UI 框架的复用,单端项目用组件库 B 更快是理所当然——比较的意义在于让你知道"效率的定价",而不是宣判谁赢;
- 组件库 B 也在演化:视图层脚本、双渲染引擎补丁说明原生路线同样要为碎片化付费,只是付费方式是"编译期补丁"而不是"运行时抽象"。
七、总表:五维度速查
| 维度 | 跨端框架 A | 原生组件库 B |
|---|---|---|
| 编译机制 | 插件式内核 + 平台模板注入;递归/逐层展开双策略 | 无编译层;构建链只做 TS→JS、less→样式,单文件约束 |
| 运行时 | 自建虚拟 DOM、路径式增量更新、批量调度、局部更新容器、模拟事件冒泡 | 零运行时;手势下沉视图层脚本;主题订阅器 |
| 组件系统 | 一份组件 + 多框架桥接包;一个应用实例管理 N 页面 | 原生构造器进阶用法:relations 直连、extClass、type:null 传函数 |
| 样式 | 转译(单位表)+ 防御(白名单/容错)+ 静态拦截(lint) | 双渲染引擎降级补丁;CSS 变量 + 观察者 + Behavior 三层主题 |
| 生命周期 | 三层映射:挂载时序保障、按需注册、钩子横向注入 | 原生裸用;存在写法混用与旧版 observer 的风格债 |
| 设计哲学 | 用编译时复杂度换运行时兼容性 | 用平台绑定换确定性 |
八、可迁移的设计结论
不管你用不用这两套方案,下面五个模式是可以直接带走复用的:
- 能力差异在编译期消化,而不是运行时 if-else——双模板策略和 新引擎补丁殊途同归:扫描真实使用情况(嵌套深度、选择器支持度),生成恰好够用的产物;
- 批量 + 去重 + 对齐渲染节奏——任何"高频小更新"场景(不只是 setData)都适用这套调度三件套,尤其是"为什么用宏任务不用微任务"这个判断;
- 钩子系统做解耦层——核心库零依赖具体实现、全部通过钩子注入,是多框架支持类库的通用架构;
- 显式扩展点优于隐式开放——extClass 模式(声明哪几个位置可定制)比全量暴露样式面更可控,组件设计通用;
- 属性传函数做依赖注入——
type: null的思路在任意属性系统里都可复用:组件定义流程骨架,异步实现外包。
收尾:怎么选
最后给一个不圆滑的结论:
- 多端是硬需求、团队有 React/Vue 背景、性能达标即可 → 框架 A 这类跨端方案,付的钱是运行时抽象,买的是复用;
- 只投一个平台、对性能/包体有硬指标、交互复杂 → 原生 + 组件库 B 这类方案,付的钱是平台绑定和人力,买的是确定性;
- 最差的选择是两头下注:先用跨端框架写,性能不达标再"混编原生"——你会同时付两种成本。
本文是「小程序双路线源码拆解」系列的第一篇。下一篇我们单拆一个点:跨端框架的增量更新调度器——路径去重的具体算法、setTimeout 调度的时序证明、局部更新容器的查找与缓存,全部源码级展开。感兴趣的话关注一下,更新会第一时间推送。
本文涉及的代码均为基于源码设计思路重写的简化伪代码,仅用于技术交流,不代表任何项目的真实实现;文中代称与真实项目无关。
更多推荐



所有评论(0)