说明:本文分析对象为两个开源项目,出于合规考虑,文中以「跨端框架 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 是"用平台绑定换确定性"。这两个极端恰好构成跨端技术光谱的两端。

读完你应该能拿到两样东西:

  1. 13 个可以单独拎出来讲的源码级技术难点——每个都附"面试话术"版本;
  2. 一个判断标准:下次选型时,你能说清楚自己到底在为什么买单。

一、跨端编译机制:转译的艺术 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 降级策略":不写两份样式,而是维护一份主样式 + 一份编译期补丁,把新引擎不支持的选择器/单位预编译展开成它支持的形式。同一平台的碎片化和跨端碎片化,解法竟然同构——都是"编译期消化能力差异",这个观察本身就能撑起一节内容。

主题方面是三层架构:

  1. 静态层:声明 darkmode: true,基础样式全部走 CSS 变量(--xx-FG-* 系列),系统切深色模式时变量自动切换——零 JS 参与;
  2. 动态层:应用入口监听系统主题变化,维护观察者数组;
  3. 混入层:一个几行的 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(有优化上限)原生数据绑定(平台原生优化)
事件链路渲染层 → 逻辑层包装 → 模拟冒泡 → 框架处理原生事件直达(交互逻辑可下沉视图层)
模板体积编译期展开 + 裁剪后仍有多层模板每组件一份手写模板,最简

但要强调两点公平性:

  1. 框架 A 的这些成本换来的是十几个平台 + 三个 UI 框架的复用,单端项目用组件库 B 更快是理所当然——比较的意义在于让你知道"效率的定价",而不是宣判谁赢;
  2. 组件库 B 也在演化:视图层脚本、双渲染引擎补丁说明原生路线同样要为碎片化付费,只是付费方式是"编译期补丁"而不是"运行时抽象"。

七、总表:五维度速查

维度跨端框架 A原生组件库 B
编译机制插件式内核 + 平台模板注入;递归/逐层展开双策略无编译层;构建链只做 TS→JS、less→样式,单文件约束
运行时自建虚拟 DOM、路径式增量更新、批量调度、局部更新容器、模拟事件冒泡零运行时;手势下沉视图层脚本;主题订阅器
组件系统一份组件 + 多框架桥接包;一个应用实例管理 N 页面原生构造器进阶用法:relations 直连、extClass、type:null 传函数
样式转译(单位表)+ 防御(白名单/容错)+ 静态拦截(lint)双渲染引擎降级补丁;CSS 变量 + 观察者 + Behavior 三层主题
生命周期三层映射:挂载时序保障、按需注册、钩子横向注入原生裸用;存在写法混用与旧版 observer 的风格债
设计哲学用编译时复杂度换运行时兼容性用平台绑定换确定性

八、可迁移的设计结论

不管你用不用这两套方案,下面五个模式是可以直接带走复用的:

  1. 能力差异在编译期消化,而不是运行时 if-else——双模板策略和 新引擎补丁殊途同归:扫描真实使用情况(嵌套深度、选择器支持度),生成恰好够用的产物;
  2. 批量 + 去重 + 对齐渲染节奏——任何"高频小更新"场景(不只是 setData)都适用这套调度三件套,尤其是"为什么用宏任务不用微任务"这个判断;
  3. 钩子系统做解耦层——核心库零依赖具体实现、全部通过钩子注入,是多框架支持类库的通用架构;
  4. 显式扩展点优于隐式开放——extClass 模式(声明哪几个位置可定制)比全量暴露样式面更可控,组件设计通用;
  5. 属性传函数做依赖注入——type: null 的思路在任意属性系统里都可复用:组件定义流程骨架,异步实现外包。

收尾:怎么选

最后给一个不圆滑的结论:

  • 多端是硬需求、团队有 React/Vue 背景、性能达标即可 → 框架 A 这类跨端方案,付的钱是运行时抽象,买的是复用;
  • 只投一个平台、对性能/包体有硬指标、交互复杂 → 原生 + 组件库 B 这类方案,付的钱是平台绑定和人力,买的是确定性;
  • 最差的选择是两头下注:先用跨端框架写,性能不达标再"混编原生"——你会同时付两种成本。

本文是「小程序双路线源码拆解」系列的第一篇。下一篇我们单拆一个点:跨端框架的增量更新调度器——路径去重的具体算法、setTimeout 调度的时序证明、局部更新容器的查找与缓存,全部源码级展开。感兴趣的话关注一下,更新会第一时间推送。


本文涉及的代码均为基于源码设计思路重写的简化伪代码,仅用于技术交流,不代表任何项目的真实实现;文中代称与真实项目无关。

Logo

一站式 AI 云服务平台

更多推荐