微信小程序双线程架构详解:逻辑层、渲染层和 setData 链路

📚 前言

上一篇我们从整体链路理解了微信小程序和小游戏:

1
2
3
4
5
6
7
8
9
开发者代码
↓
微信基础库
↓
逻辑层 / 渲染层 / 游戏运行时
↓
JSBridge
↓
微信客户端 Native

这一篇开始详细讲小程序最核心的运行模型:

1
双线程架构

小程序很多问题都和它有关:

  • 为什么不能直接操作 DOM?
  • 为什么要用 setData?
  • 为什么 setData 太频繁会卡?
  • 为什么事件回调和页面渲染不是同一个环境?
  • 为什么小程序性能优化总是在强调减少通信成本?

先把一句话放在前面:

小程序不是 JS 直接驱动 DOM,而是逻辑层通过数据通信驱动渲染层。


一、什么是双线程架构

普通 Web 页面里,JS 和 DOM 基本都在浏览器页面环境里。

你可以写:

1
document.querySelector('.title').textContent = 'hello'

JS 可以直接找到 DOM,然后修改 DOM。

但小程序不是这样。

小程序大致分为两层:

1
2
3
4
5
6
7
8
9
10
11
12
逻辑层 AppService
执行 JS 逻辑
管理 data
处理生命周期
处理事件回调
调用 wx API

渲染层 View
解析 WXML
应用 WXSS
展示页面
触发用户事件

两层不是同一个线程,也不是同一个 JS 上下文。

可以先画成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──────────────────────┐
│ 逻辑层 │
│ AppService / JS │
│ │
│ Page / Component │
│ data / methods │
│ 生命周期 │
└──────────┬───────────┘
│
│ Native 中转
│
┌──────────▼───────────┐
│ 渲染层 │
│ WebView / View │
│ │
│ WXML / WXSS │
│ 事件捕获 │
│ 页面展示 │
└──────────────────────┘

这就是所谓的双线程。

还有一个容易忽略的细节——两层的数量关系并不对称:

1
2
3
4
逻辑层:整个小程序共享「1 个」JS Runtime
所有页面、组件的 JS 都跑在这里
渲染层:「每个页面 1 个」WebView
页面栈最多约 10 层

这能顺带解释两件事:为什么页面之间可以通过逻辑层共享数据;以及为什么页面不能无限往下跳(WebView 数量是有上限的)。

一个常见误解:「双线程」不等于「只有两条线程」

“双线程”这个名字很容易让人以为:全局总共只有逻辑、渲染两条线程。其实不是。

它指的是两类角色(两类执行环境),而不是“严格只有两条线程”。真实情况是:

1
2
逻辑层:1 个 JS Runtime(所有页面共享)
渲染层:每个页面 = 1 个独立的 WebView,各自跑着一套渲染层 JS

所以如果你打开了 3 个页面,实际是:

1
2
3
1 个逻辑层 Runtime
+
3 个 WebView(页面 A / B / C 各一个,互相独立)

也就是说,至少有 4 个执行环境,而不是 2 个。那为什么还叫“双线程”?因为不管开几个页面,角色永远只有两类:

1
2
3
4
5
6
7
┌─────────────────────────────┐
│ 逻辑层(1 个,共享) │
└──────────────┬──────────────┘
│ 都经 Native 中转
┌───────────┼───────────┐
▼ ▼ ▼
WebView A WebView B WebView C ← 渲染层(N 个,每页一个)
  • 逻辑层:无论几个页面,始终只有 1 个,所有页面的 JS 都跑在这里。
  • 渲染层:是一类角色,但实例数 = 当前页面栈里的页面数,每页一个独立 WebView。

所以“双”指的是“逻辑 vs 渲染”这两类角色的分离,而不是“全局只有两条线程”。也正因为每个页面是独立 WebView(创建有开销、DOM 互相隔离),页面栈才有约 10 层的上限。


二、逻辑层负责什么

逻辑层可以理解为小程序的“业务大脑”。

你写的这些 JS,大部分都运行在逻辑层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Page({
data: {
count: 0
},

onLoad() {
console.log('page loaded')
},

handleTap() {
this.setData({
count: this.data.count + 1
})
}
})

逻辑层主要负责:

  • 执行页面 JS
  • 执行组件 JS
  • 管理页面和组件实例
  • 保存 data
  • 执行生命周期
  • 执行事件处理函数
  • 调用 wx.request、wx.login、wx.getLocation 等 API

但逻辑层不负责直接渲染页面。

它不能像浏览器页面一样:

1
document.body.appendChild(...)

因为它拿不到真实 DOM。

它只能通过框架提供的方式,把数据变化告诉渲染层。

这个方式就是:

1
this.setData(...)

三、渲染层负责什么

渲染层可以理解为小程序的“显示窗口”。

你写的:

1
2
3
<view class="counter">
{{ count }}
</view>

和:

1
2
3
.counter {
color: red;
}

对应的是:

1
2
WXML
WXSS

它们工作在渲染层。

渲染层主要负责:

  • 把 WXML 结构渲染成页面
  • 应用 WXSS 样式
  • 展示数据绑定结果
  • 捕获用户点击、输入、滚动等事件
  • 接收逻辑层传来的数据更新

这里要澄清一个常见误解:渲染层并不是“纯静态、不含 JS”的。它内部同样跑着一套微信注入的 JS(通常叫 WAWebview),负责:

1
2
3
4
5
接收逻辑层发来的数据
↓
在渲染层做虚拟 DOM 的 diff
↓
只把变化的部分 patch 到真实 DOM

也就是说,小程序其实有「两个 JS 环境」:逻辑层的 WAService 和渲染层的 WAWebview。逻辑层那套 JS 管业务和数据,渲染层这套 JS 管 diff 和事件捕获。

但渲染层不负责业务逻辑。

比如用户点击按钮:

1
<button bindtap="handleTap">+1</button>

点击事件最开始发生在渲染层。

但真正执行 handleTap 的地方,是逻辑层。

所以事件需要跨层传递。


四、Native 层负责什么

在这张图里,中间的 Native 层非常关键:

1
2
3
4
5
逻辑层 AppService
↓
Native
↓
渲染层 View

这里的 Native,可以先理解成:

1
微信客户端本身

在 iOS 上,它是微信 iOS App 的原生实现。

在 Android 上,它是微信 Android App 的原生实现。

它不是你写的小程序代码,而是微信客户端提供的宿主能力。

Native 层主要负责三类事情。

1. 做逻辑层和渲染层的中转

逻辑层和渲染层不是直接通信的。

它们之间需要经过 Native 层。

比如事件链路:

1
2
3
4
5
6
7
用户点击按钮
↓
渲染层捕获 tap
↓
Native 把事件转给逻辑层
↓
逻辑层执行 handleTap

比如视图更新链路:

1
2
3
4
5
逻辑层调用 setData
↓
Native 传输数据
↓
渲染层更新页面

所以 Native 层像一个交通枢纽:

1
2
View → Native → AppService
AppService → Native → View

2. 调用系统和微信原生能力

很多能力不是 JS 或 WebView 自己能完成的。

例如:

  • 登录
  • 支付
  • 扫码
  • 定位
  • 相机
  • 蓝牙
  • 文件系统
  • 剪贴板
  • 分享

当你写:

1
wx.scanCode()

背后大概是:

1
2
3
4
5
6
7
8
9
10
11
JS 调用 wx.scanCode
↓
微信基础库封装请求
↓
Native 收到请求
↓
Native 打开扫码能力
↓
扫码完成
↓
Native 把结果返回给 JS

所以 wx.xxx API 的本质不是普通 JS 工具函数。

它更像是:

1
JS 向微信客户端申请调用某个原生能力

3. 做权限、安全和平台适配

微信不会让页面随便调用系统能力。

Native 层会负责判断:

1
2
3
4
5
这个小程序有没有权限?
用户有没有授权?
当前 API 能不能调用?
当前平台支不支持?
参数是否合法?

比如定位、相机、蓝牙、支付,都需要微信客户端控制边界。

同时,Native 层还负责抹平平台差异。

你写:

1
wx.getLocation()

不需要关心 iOS 和 Android 底层定位 API 分别怎么调用。

微信 Native 层会在不同平台上分别适配:

1
2
iOS     → 系统定位能力
Android → 系统定位能力

最后给开发者返回一套统一结果:

1
2
3
4
5
6
{
latitude,
longitude,
speed,
accuracy
}

这就是平台适配。

4. JSBridge 在这里扮演什么角色

上面一直说「Native 中转」,但真正落到实现上,逻辑层、渲染层和 Native 之间并不是直接互相调用函数,而是通过一套桥接机制传消息。

这套桥接机制通常就叫:

1
JSBridge

可以把它理解成 JS 世界和 Native 世界之间的「通信通道 + 消息协议」。

比如逻辑层调用:

1
wx.scanCode()

它不是 JS 自己打开摄像头,而是大概经历:

1
2
3
4
5
6
7
逻辑层 JS
↓
JSBridge 把调用封装成一条消息
↓
Native 收到消息并执行扫码能力
↓
Native 把结果再通过 JSBridge 回传给 JS

再比如 setData:

1
2
3
4
5
逻辑层 setData
↓
JSBridge / Native Bridge 传输数据
↓
渲染层接收数据并更新视图

所以 JSBridge 的角色不是「第三个业务层」,而是跨运行环境通信的基础设施。它负责让 JS 能够向 Native 发请求,也让 Native 能够把事件、结果、数据更新回传给 JS 环境。

那它和 Native Bridge 是什么关系?

可以简单区分成:

1
2
JSBridge:更偏 JS 侧视角,强调 JS 怎么和 Native 通信
Native Bridge:更偏 Native 侧视角,强调 Native 怎么承接、分发、回调这些消息

很多时候两者说的是同一条桥的两个侧面:JS 侧叫 JSBridge,Native 侧叫 Native Bridge。真正重要的不是名字,而是这件事:

1
2
逻辑层、渲染层、系统能力不在同一个运行环境里
所以跨环境行为都必须经过桥接消息

在小程序里,这座桥主要承担三类消息:

  • 事件消息:渲染层捕获点击、输入、滚动,再转给逻辑层。
  • 数据消息:逻辑层 setData,再转给渲染层更新视图。
  • 能力调用消息:逻辑层调用 wx.xxx,再交给 Native 执行系统或微信能力。

五、为什么隔离逻辑层和渲染层有利于平台适配

核心原因是:

1
2
开发者面对的是微信定义的一套统一模型
而不是 iOS / Android 的底层差异

如果小程序允许开发者直接操作底层 UI 或完整 WebView DOM,那么很多差异都会暴露出来:

1
2
3
4
iOS WebView 某个布局行为不同
Android WebView 某个滚动行为不同
某些原生组件事件表现不同
不同系统权限模型不同

这样开发者就要自己处理大量兼容问题。

而小程序选择暴露:

1
2
3
4
5
6
WXML
WXSS
data
setData
生命周期
wx API

这些是微信定义出来的统一抽象。

于是平台差异可以被压到下面:

1
2
3
4
5
开发者写统一小程序模型
↓
微信基础库解释和调度
↓
Native 在不同平台分别实现

比如:

1
2
你写 wx.getLocation
微信负责在 iOS / Android 上分别接系统定位

比如:

1
2
你写 WXML / WXSS
微信负责在不同平台上渲染成对应视图

所以隔离带来的好处是:

1
2
开发者不用直接面对平台底层
微信客户端和基础库负责兜住差异

一句话:

逻辑层和渲染层隔离之后,开发者写的是统一模型;不同平台的差异,由微信 Native 和基础库在底下适配。

当然,这种适配不是没有代价。

它带来的代价就是:

1
2
3
4
不能直接操作 DOM
页面更新需要通信
setData 有成本
某些能力必须经过权限和宿主判断

这就是小程序运行时的核心取舍:

1
用受控模型换平台一致性和安全边界

六、事件是怎么从页面传到 JS 的

看一个最简单的按钮:

1
<button bindtap="handleTap">+1</button>
1
2
3
4
5
Page({
handleTap() {
console.log('tap')
}
})

底层大概是:

1
2
3
4
5
6
7
8
9
10
11
用户点击按钮
↓
渲染层捕获 tap 事件
↓
把事件信息交给微信客户端 Native
↓
Native 转发给逻辑层
↓
逻辑层找到 handleTap
↓
执行 JS 回调

也就是:

1
View → Native → AppService

注意,这里不是:

1
DOM 事件直接冒泡到 JS

而是:

1
渲染层事件 → Native 中转 → 逻辑层 JS

这就是为什么小程序事件模型看起来像 Web,但底层不是普通 Web DOM 事件。


七、setData 是怎么更新页面的

事件到了逻辑层以后,通常会修改数据:

1
2
3
4
5
6
7
8
9
10
11
Page({
data: {
count: 0
},

handleTap() {
this.setData({
count: this.data.count + 1
})
}
})

setData 做的不是简单赋值。

它大概经历:

1
2
3
4
5
6
7
8
9
10
11
逻辑层执行 setData
↓
更新逻辑层里的 data
↓
把变更数据序列化
↓
通过 Native 发送给渲染层
↓
渲染层接收数据
↓
更新 WXML 对应视图

也就是:

1
AppService → Native → View

如果把一次点击和一次页面更新连起来,就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
用户点击
↓
View 捕获事件
↓
Native 转发事件
↓
AppService 执行 JS
↓
setData 发送数据
↓
Native 转发数据
↓
View 更新页面

完整链路是:

1
View → Native → AppService → Native → View

这条链路就是小程序性能优化的核心。


八、为什么 setData 有成本

现在就能解释一个常见问题:

为什么频繁 setData 会卡?

因为 setData 不是在同一个执行环境里直接改 UI。

它涉及:

  • 数据合并
  • 数据序列化
  • 跨线程通信
  • Native 中转
  • 渲染层接收
  • 视图更新

所以每一次 setData 都有通信成本。

而且这个序列化是「双向收费」的:逻辑层要 JSON.stringify 把数据打包,渲染层收到后还要 JSON.parse 解开。数据越大,两端都越慢——这也是为什么“少传、只传变化字段”这么重要。

顺带补一个实用点:setData 的第二个参数是回调,它会在渲染层真正渲染完成后触发。想拿到“界面已更新”的准确时机,应该用它,而不是 setTimeout:

1
2
3
this.setData({ count: 1 }, () => {
// 此时渲染层已经更新完毕
})

如果你这样写:

1
2
3
4
5
for (let i = 0; i < 100; i++) {
this.setData({
count: i
})
}

就相当于短时间内向渲染层发送很多次更新消息。

更好的方式是:

1
2
3
this.setData({
count: 99
})

也就是:

1
2
3
能合并就合并
能少传就少传
能只传变化字段就只传变化字段

九、setData 的常见性能问题

1. 传太多数据

不推荐:

1
2
3
this.setData({
list: hugeList
})

如果 hugeList 很大,每次都整体传输,通信成本会很高。

更好的方式是只更新变化部分:

1
2
3
this.setData({
[`list[${index}].checked`]: true
})

核心原则:

1
不要每次把整棵数据树都发给渲染层

2. 调用太频繁

比如滚动、拖拽、输入时频繁更新:

1
2
3
4
5
onPageScroll(e) {
this.setData({
scrollTop: e.scrollTop
})
}

如果每一帧都 setData,就容易造成通信拥堵。

应该考虑:

  • 节流
  • 防抖
  • 只在关键状态变化时更新
  • 能用 CSS / 原生组件能力解决就不要频繁走 JS

3. 更新不参与渲染的数据

不推荐把所有状态都放进 data。

因为 data 更适合放:

1
需要驱动视图渲染的数据

如果只是逻辑内部变量,可以放在实例字段上:

1
2
3
4
5
6
7
Page({
timer: null,

onLoad() {
this.timer = setInterval(() => {}, 1000)
}
})

不要什么都:

1
2
3
this.setData({
timer: xxx
})

十、为什么不能直接操作 DOM

现在再看这个问题:

小程序为什么不能直接操作 DOM?

因为小程序的逻辑层和渲染层是隔离的。

逻辑层没有浏览器页面里的 DOM 对象。

你不能假设:

1
document.querySelector('.box')

可以拿到页面节点。

小程序更希望你使用:

1
数据驱动 UI

也就是:

1
2
3
4
5
data 变化
↓
setData
↓
渲染层更新

而不是:

1
2
3
JS 直接拿 DOM
↓
手动改 DOM

这让小程序可以:

  • 控制运行环境
  • 控制安全边界
  • 做跨平台适配
  • 统一渲染模型
  • 限制危险能力

代价是:

1
开发者失去了直接操作 DOM 的自由

十一、双线程架构带来的好处

双线程不是只有限制。

它也带来一些好处:

不过这里要先澄清一个容易混淆的点:

1
2
双线程不是让每一次调用都更快
而是用通信成本换取更稳定的渲染、更清晰的安全边界

也就是说,它提升的不是「单次 setData 的速度」,而是整个小程序运行模型的可控性。

1. 安全性更强

开发者代码不会直接接触完整浏览器环境。

微信可以控制哪些能力开放,哪些能力不开放。

更具体地说,逻辑层不能直接操作真实 DOM,渲染层也不能绕过逻辑层随便调用业务能力。所有跨边界行为都要经过 Native / JSBridge,微信才有机会做权限判断、参数校验、平台规则限制和用户授权确认。

2. 更容易跨平台

微信要同时运行在 iOS、Android、Windows、macOS 等环境。

统一的运行模型可以抹平一部分平台差异。

3. 原生能力更容易接入

所有 wx.xxx 能力都可以通过微信客户端统一管理。

比如:

  • 登录
  • 支付
  • 扫码
  • 定位
  • 文件
  • 蓝牙
  • 相机

这些都不需要直接暴露底层系统 API 给页面。

4. 渲染和逻辑可以隔离

逻辑层主要处理业务,渲染层主要处理显示。

这种分层能让微信框架更容易管理页面生命周期和通信。

5. 渲染更不容易被业务 JS 拖死

普通 Web 页面里,业务 JS、DOM 操作、样式计算、布局和绘制往往都挤在同一个页面环境里。只要 JS 里有复杂计算、大循环、频繁同步读写 DOM,就可能阻塞页面渲染,表现出来就是点击没反应、滚动卡顿、动画掉帧。

小程序把逻辑层和渲染层拆开后,业务 JS 不再直接控制真实 DOM,也不能随意插入大量同步 DOM 操作。页面更新必须变成数据消息:

1
2
3
4
5
逻辑层改数据
↓
setData 发送变化
↓
渲染层统一更新视图

这样一来,微信框架可以把页面更新收敛到受控链路里,做数据合并、调度、节流和渲染层 diff。代价是通信变多了,但好处是渲染节奏更可控,不容易被开发者的业务 JS 直接拖垮。

所以这里的性能收益不是:

1
通信更少,所以更快

而是:

1
通信更多,但渲染和业务逻辑互相隔离,整体卡顿风险更可控

这也解释了为什么小程序一边采用双线程,一边又反复强调减少 setData:双线程解决的是架构隔离问题,不代表跨线程通信没有成本。


十二、双线程架构带来的代价

当然,它也有明显代价。

1. 通信成本

逻辑层和渲染层之间不能直接共享数据。

所有更新都要经过通信。

这就是 setData 成本的来源。

2. 调试心智更复杂

普通 Web 出问题时,可能只需要看 DOM 和 JS。

小程序出问题时,需要想清楚:

1
2
3
4
5
是逻辑层数据错了?
是 setData 没发出去?
是渲染层没更新?
是组件生命周期没触发?
还是 Native 能力调用失败?

3. 框架封装会增加一层心智

如果再叠加 uni-app、Taro:

1
2
3
4
5
6
7
Vue / React 源码
↓
跨端框架编译和运行时
↓
小程序逻辑层 / 渲染层
↓
微信基础库

问题定位会再多一层。

所以使用跨端框架时,更要理解底层链路。


十三、uni-app / Taro 下的 setData 心智

用 uni-app 或 Taro 时,你可能不会直接写很多 this.setData。

比如 uni-app 里更像 Vue:

1
2
3
4
5
6
7
8
9
<template>
<view>{{ count }}</view>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

Taro 里更像 React:

1
2
3
4
function Page() {
const [count, setCount] = useState(0)
return <View>{count}</View>
}

但在微信小程序端,最终还是要把状态变化同步给小程序渲染层。

也就是说:

1
2
3
4
5
6
7
Vue / React 状态变化
↓
框架运行时或编译产物处理
↓
映射到小程序数据更新
↓
渲染层更新视图

所以你不直接写 setData,不代表底层没有类似成本。

跨端框架只是帮你隐藏了一部分细节。

底层约束仍然存在:

  • 状态更新太频繁仍然可能卡
  • 渲染节点太多仍然会慢
  • 大数据更新仍然有通信成本
  • 平台能力仍然受微信宿主限制

十四、怎么判断问题出在哪一层

遇到小程序页面卡顿或更新异常,可以按这个顺序排查:

1
2
3
4
5
6
7
1. 数据是否真的变化了?
2. 是否触发了 setData 或框架状态更新?
3. 更新的数据是否过大?
4. 更新频率是否过高?
5. 渲染层节点是否过多?
6. 是否有跨端框架编译或运行时适配问题?
7. 是否是微信基础库或平台差异?

如果用链路图表示:

1
2
3
4
5
6
7
8
9
业务代码
↓
框架状态
↓
小程序 data 更新
↓
Native 通信
↓
渲染层更新

你要做的是沿着这条链路向下定位。

不要一上来就只怀疑:

1
2
3
是不是微信抽风?
是不是 Taro bug?
是不是 uni-app bug?

当然它们可能有 bug。

但更常见的问题,是我们没有意识到:

1
上层写法最终还是要落到小程序双线程模型里

十五、新趋势:Skyline 渲染引擎

前面整篇都基于一个前提:渲染层 = WebView。这是小程序最初、也是目前默认的渲染方式。但要知道,微信已经推出了新的渲染引擎——Skyline。

它和 WebView 模式最大的区别是:

1
2
WebView 模式:渲染层是 WebView,用浏览器内核渲染 WXML / WXSS
Skyline 模式:渲染层换成微信自研的原生渲染管线,绕开 WebView

Skyline 的目标是让渲染更接近原生——更流畅的动画、更低的内存开销。

但关键要记住一点:

双线程模型本身没有变。 逻辑层(JS、数据)和渲染层(显示)依旧是分离的,依旧通过通信驱动渲染。变的只是渲染层的「实现」——从 WebView 换成了 Skyline。

所以本篇讲的 setData 通信链路、数据驱动、不能直接操作 DOM 这些核心心智,在 Skyline 下同样成立。Skyline 优化的是“渲染层怎么把画面画出来”,而不是“双线程怎么通信”这件事本身。

Worklet:给双线程开一条「绿色通道」

双线程有个天生的死穴:动画和手势对帧率极其敏感,但每一帧都要走 渲染层 → Native → 逻辑层 → setData → Native → 渲染层 这一大圈。60fps 就是每秒 60 次跨线程通信,根本跟不上手指。

1
2
传统:手指拖动 → 渲染层捕获 → Native → 逻辑层算位置 → setData → Native → 渲染层
(每帧都绕一大圈,必然掉帧)

Skyline 用 Worklet 解决这个问题。它实现的效果是:让一小段 JS 直接跑在渲染线程上,动画/手势不再回逻辑层。

1
2
Skyline:手指拖动 → 渲染线程上的 worklet 直接算 + 直接更新画面
(不跨线程,跟手丝滑)

它具体怎么做到的:

  • 用 'worklet' 指令标记函数:被标记的函数会被单独抽取,能在渲染线程的 JS 环境里执行(这套设计和 React Native 的 Reanimated 几乎一致)。
  • 用「共享值」(SharedValue) 跨线程传数据:共享值两个线程都能读写,UI 线程直接改它来驱动动画,全程不碰逻辑层。
  • 需要时再用 runOnJS 回逻辑层:比如手势结束、要发请求时,才跨一次线程通知业务逻辑。

所以 Worklet 不是推翻双线程,而是给它开了条绿色通道:绝大部分业务逻辑仍在逻辑层,只有对帧率敏感的动画/手势被下放到渲染线程,绕开通信瓶颈。

代价:需要渐进迁移

Skyline 不是开个开关就免费用:

  • 需主动开启:在 app.json 配 "renderer": "skyline",可按页面选择。
  • CSS / 组件有限制:不支持全部 CSS(布局更偏原生,默认 flex 等行为不同),部分老组件要换成 Skyline 适配版。
  • 有基础库版本要求:低版本微信会回退到 WebView。

所以微信支持两种模式混用——新页面用 Skyline、老页面留 WebView,逐步迁移。


🧠 一句话总结

小程序双线程架构的核心是:

1
2
3
逻辑层负责 JS 和数据
渲染层负责页面显示
两者通过微信 Native 中转通信

所以页面更新不是:

1
JS 直接改 DOM

而是:

1
2
3
4
5
6
7
JS 更新数据
↓
setData / 框架状态同步
↓
Native 中转
↓
渲染层更新

理解这条链路,就能解释:

  • 为什么不能直接操作 DOM
  • 为什么 setData 有成本
  • 为什么频繁更新会卡
  • 为什么跨端框架仍然绕不开小程序底层模型

下一篇可以继续讲:

1
JSBridge 和 wx API:小程序如何调用微信原生能力