9.响应式数据绑定设计
# 9.响应式数据绑定设计
📍 本篇位置:第 5 卷 · 交互与系统 · 第 9 篇(承接 5.2「视图加载」、5.3「图形渲染」、5.7「生命周期」、5.8「路由」——5.2 解决「视图怎么挂上去」,5.3 解决「像素怎么画出来」,5.7 解决「容器怎么活怎么死」,5.8 解决「页面之间怎么跳」,5.9 解决「数据变了视图怎么自动变」这个"最后一公里") 🎯 核心矛盾:「业务想专注于数据 What」 vs 「视图必须知道怎么变 How」——产品改一个字段,UI 上 20 处都得跟着变;用户输入 + 网络回包 + 后台推送三路数据涌入,视图必须保持一致。命令式时代我们用
setText/setVisibility/notifyDataSetChanged一处处手动同步,结果就是「改 A 处忘 B 处」「多个数据源打架」「列表抖动闪烁」。解法只有一个:把"数据变化 → 视图更新"这条因果链由"人写"变成"框架自动推"——这就是响应式(Reactive)数据绑定 🧭 设计灵魂:响应式 = 「可观察数据源 + 依赖收集 + 变更通知 + 视图调度 + 差异比对 + 生命周期绑定」六件套。任何响应式框架(KVO/RxJava/RxSwift/LiveData/Flow/Combine/Vue Reactivity/MobX/Compose Snapshot/SwiftUI @State/Signal/Solid)都在这六件套上做选择——理解了通用骨架,所有平台的"双向绑定 / 单向数据流 / 数据驱动 UI"都是同一棵树的不同枝叶 🌐 跨平台覆盖:Web (Vue 2 Object.defineProperty / Vue 3 Proxy · React useState/useEffect · MobX · RxJS · Solid Signal · Svelte Rune) · Android (DataBinding · LiveData · StateFlow/SharedFlow · Compose State · RxJava) · iOS (KVO · RxSwift · Combine · SwiftUI @State/@Observable · ReactiveCocoa) · 跨端 (Flutter setState/Provider/Riverpod/GetX · React Native · Compose Multiplatform · 小程序 setData/MobX-miniprogram) · 鸿蒙 ArkTS @State/@Link/@Provide 🔗 延伸阅读:← 5.2 视图加载渲染设计 (opens new window) · ← 5.3 图形渲染管线原理 (opens new window) · ← 5.5 消息机制设计思想 (opens new window) · ← 5.7 组件生命周期管理 (opens new window) · ← 5.8 页面导航与路由设计 (opens new window) · → 5.10 数据加密和解密 (opens new window) · ↔ 6.x 架构思想 MVVM/MVI 💡 通用心智:忘掉binding.tvName = "xxx"/notifyDataSetChanged/setState/setData这些 API,记住一句话——响应式 = 「数据是真相(Source of Truth),视图是数据的函数(UI = f(state))」。Vue 的 ref、React 的 useState、SwiftUI 的 @State、Compose 的 mutableStateOf、Flutter 的 ValueNotifier、LiveData/StateFlow、小程序的 data——本质都是把"手写 UI 更新代码"这件事自动化、可追踪、可调度、可批量、可时间旅行。核心理解:① 响应式不是"绑定"——是"依赖收集 + 自动通知"的发布订阅;② 数据必须可观察——读取被代理收集依赖,写入被代理触发通知;③ 视图必须是"幂等纯函数"——给同样的 state 必须渲出同样的 UI,否则会失去等价替换的能力——把这三点想明白,所有数据驱动 UI 的问题就有答案了。
# 目录
- 1.数据不同步事故
- 2.为什么需要响应式
- 3.响应式六件套契约
- 4.Web 响应式设计
- 5.Android 响应式
- 6.iOS 响应式体系
- 7.跨端响应式设计
- 8.横向对比矩阵
- 9.反模式与陷阱
- 10.现代演进路线
- 11.心智与设计哲学
# 1.数据不同步事故
# 1.1 真实事故案例
某电商 App 用户反馈:「我在购物车把商品数量从 2 改成 5,结算页价格还是按 2 算的,下单金额对不上!」
故障复现的代码(典型的命令式更新):
// CartActivity.kt(命令式 · 反例)
class CartActivity : Activity() {
private var quantity = 1
private var unitPrice = 99.0
private var totalPrice = 99.0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.btnPlus.setOnClickListener {
quantity++ // ① 改了一个数据
binding.tvQuantity.text = "$quantity" // ② 手动同步 UI 1
totalPrice = unitPrice * quantity // ③ 手动算派生数据
binding.tvTotal.text = "¥$totalPrice" // ④ 手动同步 UI 2
// ❌ 漏了:埋点上报、库存校验、底部 sticky 栏的总价、Toolbar 商品数
// 购物车 Tab 红点……一处改动 5 个地方要跟,工程师一定会漏
}
binding.btnCheckout.setOnClickListener {
// 💣 BUG:从内存读 totalPrice 而不是重算,且 quantity 是 Activity 私有
// 跳到结算页时传的是另一个全局 CartManager.quantity
val intent = Intent(this, CheckoutActivity::class.java)
intent.putExtra("price", CartManager.totalPrice) // 数据源不一致
startActivity(intent)
}
}
}
object CartManager {
var quantity: Int = 1 // 全局单例的另一个数据源
var totalPrice: Double = 99.0
// 没有任何机制保证 CartActivity 的 quantity 和这里同步
}
故障链:
- 用户点击
+,CartActivity.quantity从 2 → 5,tvQuantity和tvTotal都更新了 - 但
CartManager.quantity没有被通知(两个数据源各活各的) - 用户点结算 →
intent.putExtra("price", CartManager.totalPrice)传的是旧值 99×2 = 198 - 结算页显示 198,用户实付按 198 扣款;但订单系统按数据库 quantity=5 发了 5 件货
- 下单 1 个月内 GMV 损失 230 万,客诉 1.2 万单,运营手工补差价 + 道歉信
# 1.2 表面与根因修复
错误的修复方式(治标不治本):
// ❌ 反例:哪里漏了补哪里
binding.btnPlus.setOnClickListener {
quantity++
binding.tvQuantity.text = "$quantity"
totalPrice = unitPrice * quantity
binding.tvTotal.text = "¥$totalPrice"
CartManager.quantity = quantity // 补一刀
CartManager.totalPrice = totalPrice // 再补一刀
binding.toolbarBadge.text = "$quantity" // 又补一刀
binding.bottomStickyPrice.text = "¥$totalPrice" // 还补一刀
AnalyticsTracker.report("cart_qty_change", quantity) // 埋点
// ……再过 3 个月,PM 新增"加车送优惠券提示",工程师再补一刀,再漏一个地方
}
这就是「命令式更新」的死亡螺旋——每次需求迭代都在原地累积维护成本,bug 永远修不完。
根因修复(响应式):
// ✅ 正例:声明"数据 → UI"的因果,让框架自动同步
class CartViewModel : ViewModel() {
val quantity = MutableStateFlow(1) // 唯一真相源
val unitPrice = MutableStateFlow(99.0)
// 派生数据 = 数据的函数(自动重算)
val totalPrice: StateFlow<Double> = combine(quantity, unitPrice) { q, p ->
q * p
}.stateIn(viewModelScope, SharingStarted.Eagerly, 99.0)
fun plus() { quantity.value++ } // 只改"真相",不改 UI
}
// CartActivity.kt
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 一处声明,N 处自动同步
launch { vm.quantity.collect { binding.tvQuantity.text = "$it" } }
launch { vm.totalPrice.collect { binding.tvTotal.text = "¥$it" } }
launch { vm.quantity.collect { binding.toolbarBadge.text = "$it" } }
// 结算页跳转:从同一个 ViewModel 读,永远一致
}
}
或者更彻底地用 Compose / SwiftUI / Vue 的声明式写法,连 collect 都省了:
// Compose 写法:UI 就是 state 的函数
@Composable
fun CartScreen(vm: CartViewModel) {
val qty by vm.quantity.collectAsState()
val total by vm.totalPrice.collectAsState()
Column {
Text("数量: $qty")
Text("总价: ¥$total")
Button(onClick = { vm.plus() }) { Text("+") }
}
// ✨ 无须 setText、无须 notify、无须手动同步——框架自动重组只更新变化的部分
}
# 1.3 事故核心启示
| 维度 | 命令式(事故方式) | 响应式(修复方式) |
|---|---|---|
| 真相源 | 多份散落(Activity / Manager / Intent) | 单一可观察源(StateFlow) |
| 派生数据 | 手动计算 + 手动同步 | 自动 derive,永远新鲜 |
| 多 UI 同步 | 一个数据 N 个 setText | 一次 collect / 一次声明 |
| 新增订阅者 | 改一处加一处 | 新代码 collect 即可,老代码不动 |
| 错误模式 | 改 A 忘 B(编译器查不出来) | 数据动了订阅者必然收到 |
| 可测试性 | UI 和数据耦合,难 mock | ViewModel 纯逻辑,单元测试覆盖 |
一句话:响应式不是为了"少写代码",而是为了让"数据一致性"从"靠程序员的记忆"变成"靠框架的契约"——这才是它的真正价值。
# 2.为什么需要响应式
# 2.1 命令式三宗罪
# 2.2 散弹枪式修改
一个数据变化,需要在 N 个地方手动同步:
// ❌ 命令式:用户名改一处,UI 上 8 处都得跟
function updateUserName(newName) {
document.getElementById('header-name').innerText = newName;
document.getElementById('sidebar-name').innerText = newName;
document.getElementById('profile-name').value = newName;
document.getElementById('greeting').innerText = `Hello, ${newName}`;
document.getElementById('breadcrumb').innerText = newName;
document.title = `${newName} - MyApp`;
localStorage.setItem('userName', newName);
analytics.report('name_change', { name: newName });
// ……新加一处 UI,必须改这个函数,否则就漏同步
}
根本问题:「数据 → UI」这条因果链被打散成 N 条手写的赋值语句,没有任何机制保证一致性。需求迭代越多,这种"哪里漏改了"的 bug 越多。
# 2.3 派生数据陈旧
// ❌ 反例:派生数据需要手动重算
var firstName: String = "张"
var lastName: String = "三"
var fullName: String = "张三" // 派生数据
fun setFirstName(name: String) {
firstName = name
fullName = firstName + lastName // 必须记得重算
// 如果忘了重算,fullName 就过期了
}
// 一个月后另一个工程师写了
fun setLastName(name: String) {
lastName = name
// 💣 忘了重算 fullName,bug 上线
}
根本问题:派生数据(Computed / Derived)应该是「输入数据的函数」,命令式让它退化成「必须手动维护的另一份独立数据」,破坏了纯函数性质。
# 2.4 多源数据打架
// ❌ iOS 反例:购物车数量有 3 个真相
class CartViewController: UIViewController {
var quantity: Int = 1 // 真相 A:VC 内
@IBOutlet var quantityLabel: UILabel! // 真相 B:UI 文本
// 真相 C:CartManager.shared.quantity(全局单例)
// 真相 D:服务端的 cart.quantity(数据库)
// 真相 E:UserDefaults 里缓存的 cached_quantity
// 5 个真相,谁是真的?什么时候同步?哪个先变?
// 当用户在 A 页改数量,B 页刷新看到旧数,再到 C 页结算,
// 拿到哪个值?没人说得清。
}
根本问题:多份数据各活各的,没有"谁是权威"的契约。一旦在某个分支忘了同步,整个 App 进入"薛定谔状态",每个页面看到的可能都不一样。
# 2.5 观察者到响应式
响应式不是凭空冒出来的,它是 30 年来"如何让数据 → UI 这条链路自动化"这条主线上的演化终点。
1995 GoF《设计模式》:观察者模式(Observer)
Subject.attach(Observer) / Subject.notify()
👇 手动 attach / 手动 notify,每对 Subject-Observer 都要写胶水
1997 Java Bean 的 PropertyChangeListener
firePropertyChange("name", old, new)
👇 标准化了 fire 的格式,但还是要手写 fire 调用
2002 .NET INotifyPropertyChanged + WPF 双向绑定(XAML)
<TextBox Text="{Binding Name, Mode=TwoWay}" />
👇 第一次实现了 "声明式绑定",但还要手动 implement 接口
2009 Knockout.js(Web 第一次响应式尝试)
this.firstName = ko.observable("Alice");
👇 用 ko.observable 包装,调用 .subscribe() 订阅变化
2010 Rx(Reactive Extensions),微软出品,后移植到 RxJava/RxSwift/RxJS
Observable.fromEvent(...).map(...).filter(...).subscribe(...)
👇 把"事件流"抽象为可组合的代数操作,但学习曲线陡峭
2013 React:useState + setState 触发 VDOM diff
👇 视图 = f(state),但 setState 还是命令式调用
2014 Vue.js(Evan You):Object.defineProperty 实现自动依赖收集
this.message = 'hello' // 直接赋值即触发更新
👇 第一次让"普通赋值"也能触发响应式,开发者体验飞跃
2017 MobX:透明的响应式(@observable + @observer)
👇 把 OOP 风格的响应式做到极致
2019 SwiftUI + Combine:@State / @Published 声明式
👇 Apple 把响应式写进了语言级注解
2020 Jetpack Compose + Snapshot:Google 的声明式答卷
2021 Vue 3 Proxy + Composition API
2022 Solid.js / Qwik:细粒度 Signal,无 VDOM
2023 Svelte 5 Rune / Vue Vapor:编译时响应式,运行时零开销
2024 React 19 Compiler:自动 memo,命令式语法的响应式编译
主线:让"程序员声明 What",让"框架决策 How"——这就是 30 年的演化方向。
# 2.6 响应式设计目标
任何一个响应式框架(无论叫 LiveData、StateFlow、Observable、Signal、@State)都要回答以下五个问题:
| 设计目标 | 解决的痛点 | 典型实现 |
|---|---|---|
| ① 唯一真相源 | 多数据源打架 | ViewModel/Store/State 集中管理 |
| ② 自动依赖收集 | 散弹枪修改 | 读取时记录依赖(Proxy/defineProperty/Signal) |
| ③ 自动通知更新 | 派生数据过期 | 写入时遍历依赖触发 callback |
| ④ 批量调度 | 多次小更新引发抖动 | 异步队列 + 合并(nextTick / Snapshot) |
| ⑤ 生命周期联动 | 内存泄漏 / 死引用 | LifecycleOwner / DisposeBag / WeakRef |
记住这五点——后面所有平台(Vue/React/Compose/SwiftUI/Flutter/小程序)的实现都是这五点的不同排列组合,理解了通用骨架,所有具体 API 都只是命名差异。
# 3.响应式六件套契约
任何响应式系统,无论叫什么名字(Vue Ref、React useState、SwiftUI @State、Compose mutableStateOf、Flutter ValueNotifier、LiveData、StateFlow),骨架都是同一套六件套:
┌────────────────────────────────────────────────────────────────────┐
│ ① 可观察数据源(Observable)─ 数据被代理/包装,可读可写 │
│ ↓ 读 ↑ 写 │
│ ② 依赖收集(Tracking) ③ 变更通知(Notify) │
│ 读取时把"当前订阅者" 写入时遍历依赖列表 │
│ 记入数据的依赖列表 调用每个订阅者的 callback │
│ ↓ ↓ │
│ ④ 视图调度(Schedule)─ 把多个变更合并到一帧,避免抖动 │
│ ↓ │
│ ⑤ 差异比对(Diff)─ 计算新旧 UI 树的最小变更集 │
│ ↓ │
│ ⑥ 生命周期绑定(Lifecycle)─ 页面销毁时自动解除订阅,防泄漏 │
└────────────────────────────────────────────────────────────────────┘
下面逐件拆解。
# 3.1 可观察数据源
契约:数据不能是裸露的字段,必须被包装成"可监听的容器"。
不同平台的实现方式:
| 平台 | 容器形式 | 拿到值的方式 | 修改值的方式 |
|---|---|---|---|
| Vue 2 | data() { return { count: 0 } } | this.count | this.count = 1 |
| Vue 3 | const count = ref(0) | count.value | count.value = 1 |
| React | const [count, setCount] = useState(0) | count | setCount(1) |
| MobX | @observable count = 0 | count | count = 1 |
| Solid | const [count, setCount] = createSignal(0) | count() | setCount(1) |
| Svelte 5 | let count = $state(0) | count | count = 1 |
| LiveData | val count = MutableLiveData(0) | count.value / observe | count.value = 1 |
| StateFlow | val count = MutableStateFlow(0) | count.value / collect | count.value = 1 |
| Compose | val count = mutableStateOf(0) | count.value | count.value = 1 |
| SwiftUI | @State var count = 0 | count | count = 1 |
| Combine | @Published var count = 0 | count / $count.sink | count = 1 |
| Flutter | ValueNotifier(0) | .value / ValueListenableBuilder | .value = 1 |
| 小程序 | data: { count: 0 } | this.data.count | this.setData({ count: 1 }) |
| 鸿蒙 ArkTS | @State count: number = 0 | this.count | this.count = 1 |
两种代理技术路线:
| 路线 | 代表 | 优点 | 缺点 |
|---|---|---|---|
| 属性级代理(getter/setter) | Vue 2 (defineProperty), Knockout | 兼容性好 | 无法监听新增/删除属性、数组下标 |
| 对象级代理(Proxy) | Vue 3, MobX 6+ | 监听完整、性能好 | IE 不支持,必须 ES6+ |
| 显式 API(setter 函数) | React, Solid, SwiftUI, Compose | 性能极致、无魔法 | 写起来啰嗦 |
| 编译时改写 | Svelte, Vue Vapor | 运行时零开销 | 依赖编译器 |
# 3.2 依赖收集机制
契约:读取数据时,必须知道"现在是谁在读",把这个读取者记入该数据的"依赖列表"。
通用伪代码:
// 全局变量:当前正在执行的 effect(订阅者)
let currentEffect: Effect | null = null;
class Observable<T> {
private value: T;
private deps = new Set<Effect>(); // 依赖列表
get(): T {
if (currentEffect) {
this.deps.add(currentEffect); // ✨ 关键:把读取者加入依赖
currentEffect.dependencies.add(this); // 双向记录,便于解绑
}
return this.value;
}
set(newValue: T) {
if (this.value === newValue) return; // 值未变,不触发(重要优化)
this.value = newValue;
// 通知所有依赖(②.3)
this.deps.forEach(effect => effect.run());
}
}
class Effect {
dependencies = new Set<Observable<any>>();
constructor(private fn: () => void) {
this.run();
}
run() {
// 1. 清除旧依赖(防止条件分支带来的"死依赖")
this.dependencies.forEach(dep => dep.deps.delete(this));
this.dependencies.clear();
// 2. 把自己设为"当前 effect",执行 fn,期间所有 get() 都会收集到这里
currentEffect = this;
try { this.fn(); } finally { currentEffect = null; }
}
}
// 用法
const count = new Observable(0);
new Effect(() => {
console.log('count is', count.get()); // 这次读取被收集
});
count.set(1); // 自动重跑上面的 console.log
这就是 Vue 3 / MobX / Solid 的核心——名字不一样(叫 effect / autorun / createEffect),原理完全相同。
两种收集模型:
| 模型 | 代表 | 特点 |
|---|---|---|
| 细粒度自动收集 | Vue, MobX, Solid, Signal | 读哪个收哪个,最精准 |
| 粗粒度手动声明 | React useEffect(fn, [deps]) | 程序员手动写依赖数组,IDE/lint 检查 |
| 快照对比 | Compose, React VDOM | 不收集,整棵树重渲染 + diff |
# 3.3 变更通知机制
契约:写入触发遍历依赖列表,调用每个订阅者的 callback。
最朴素的实现:
set(newValue) {
this.value = newValue;
this.deps.forEach(effect => effect.run()); // 同步通知
}
但生产级实现必须考虑三个问题:
# 3.4 同步还是异步
// ❌ 同步通知的灾难
count.value = 1; // 触发 100 次 render
count.value = 2; // 又触发 100 次 render
count.value = 3; // 又触发 100 次 render
// 一帧内做了 300 次重渲染,浏览器卡死
解法:异步批处理。
// ✅ Vue 3 的做法:把所有变更推入微任务队列,下一个 tick 统一处理
function scheduleJob(effect) {
if (!pendingJobs.has(effect)) {
pendingJobs.add(effect);
Promise.resolve().then(flushJobs); // 微任务
}
}
# 3.5 父子通知顺序
父组件依赖 store.user
子组件依赖 store.user.name
当 store.user = newUser 时,应该先重渲父还是子?
答案:必须先父后子(自顶向下),否则子组件先渲完成后父组件又用 newUser 重渲一次,子组件被销毁重建——产生闪烁。
实现:Vue 3 给每个 effect 一个 id(创建顺序),调度时按 id 升序排序。
# 3.6 循环依赖处理
const a = ref(0);
const b = ref(0);
watchEffect(() => { a.value = b.value + 1; });
watchEffect(() => { b.value = a.value + 1; });
// 💀 a 变 → 触发 effect2 改 b → 触发 effect1 改 a → 无限循环
解法:检测循环。Vue 3 在调度时记录每个 effect 的执行次数,超过 100 次抛错;MobX 用 enforceActions 强制只能在 action 内写。
# 3.7 视图调度批量
契约:把"数据变化 → 视图更新"这条链路解耦——数据变化立即记录,视图更新延后批量执行。
时刻 T0: count.value = 1 ──┐
时刻 T0: count.value = 2 ├──► 入队但不立刻渲染
时刻 T0: count.value = 3 ──┘
↓
时刻 T1: 一帧结束/微任务执行 ──► 取队列里最后一次状态(count=3),一次性渲染
不同框架的"调度时机":
| 框架 | 调度时机 | 实现 |
|---|---|---|
| Vue 3 | 微任务(Promise.resolve) | 批量去重,nextTick |
| React | 事件循环结束 / flushSync 强制同步 | Fiber 中断式调度 + 优先级 |
| MobX | runInAction 包裹的代码块结束 | 显式事务边界 |
| SwiftUI | RunLoop 的 beforeWaiting | 主线程合并 |
| Compose | Recomposer 的下一个 frame | Snapshot.notifyObjectsInitialized |
| LiveData | 主线程 Handler.post | postValue 异步,setValue 同步 |
| 小程序 | setData 触发 IPC,渲染线程合并 | 跨进程批量 |
React 18 的关键升级:Automatic Batching——在 Promise、setTimeout、原生事件回调里多次 setState 也会自动批处理,不像 17 之前只在 React 事件里批处理。
# 3.8 差异比对算法
契约:调度触发渲染后,框架必须计算"新视图 vs 旧视图"的差异,只把"变了的部分"打补丁到真实 UI,避免整树重建。
state 变化
↓
[执行 render 函数] ──► 产生新的虚拟 UI 树(VDOM / NodeTree)
↓
[diff 新旧两棵树] ──► 算出最小变更集 patches
↓
[apply patches] ──► 只更新真实 UI 中变了的节点
主流 diff 策略:
| 策略 | 代表 | 复杂度 | 特点 |
|---|---|---|---|
| VDOM diff | React, Vue, RN | O(n) | 同层比较,key 优化 |
| 细粒度 Signal(无 diff) | Solid, Qwik, Preact Signal | O(1) | 直接订阅 DOM 节点,跳过 diff |
| 快照系统 | Compose, SwiftUI | O(k) (k=变化节点) | 智能跳过未变 Composable |
| 脏检查 | 小程序 setData, AngularJS | O(n²) | 性能差,但实现简单 |
key 的重要性(VDOM 派的核心优化):
// ❌ 反例:用 index 当 key
{list.map((item, i) => <li key={i}>{item.name}</li>)}
// 删除中间一项时,所有 key 都错位,导致 DOM 全部重建
// ✅ 正例:用稳定的业务 id 当 key
{list.map(item => <li key={item.id}>{item.name}</li>)}
// diff 算法能识别"哪个节点被删了",只删一个 DOM
# 3.9 生命周期绑定
契约:订阅必须有"自动解除"的契机,否则订阅者一直挂着,被订阅对象引用着,永远无法 GC,造成内存泄漏。
经典翻车案例(Android):
// ❌ LiveData 之前的写法
class MyActivity : Activity() {
override fun onCreate(s: Bundle?) {
UserManager.userNameListener = { name ->
findViewById<TextView>(R.id.name).text = name // 持有 Activity!
}
}
// 用户旋转屏幕:Activity 被销毁重建
// 但 UserManager 仍持有旧 Activity 的 listener
// 旧 Activity 被泄漏,新 Activity 又注册一次 → 两份 listener
// 屏幕转 10 次,泄漏 10 个 Activity,OOM
}
响应式框架的解法:
| 平台 | 自动解绑机制 |
|---|---|
| Android LiveData | 绑定 LifecycleOwner,Activity onDestroy 自动 remove |
| Kotlin Flow | lifecycleScope.launch { repeatOnLifecycle } |
| iOS Combine | cancellables Set + 持有它的对象释放时取消 |
| RxSwift | DisposeBag,bag 释放时所有订阅取消 |
| Vue / React | 组件卸载(onUnmounted / useEffect 返回 cleanup)时自动停止 effect |
| Compose | DisposableEffect + Composable 离开树时清理 |
| SwiftUI | @State 的生命周期跟 View 绑定,View 消失自动释放 |
| Flutter | StatefulWidget.dispose() 中手动解绑 |
通用规则:所有订阅必须有"持有者",持有者死了订阅必须跟着死——这是响应式框架不爆内存的根本。
# 3.10 跨端名词矩阵
把上面六件套在八个主流端的命名串成一张大表:
| 六件套 \ 端 | Web Vue 3 | Web React | Android Compose | iOS SwiftUI | Flutter | RN | 小程序 | 鸿蒙 ArkTS |
|---|---|---|---|---|---|---|---|---|
| ① 可观察源 | ref(), reactive() | useState | mutableStateOf | @State | ValueNotifier | useState | data | @State |
| ② 派生数据 | computed | useMemo | derivedStateOf | computed property | Provider.select | useMemo | computed(WXS) | @Computed |
| ③ 副作用 | watchEffect | useEffect | LaunchedEffect | .onChange | addListener | useEffect | observers | @Watch |
| ④ 跨组件共享 | provide/inject/Pinia | Context/Redux | CompositionLocal | @Environment | Provider/Riverpod | Context | globalData | @Provide/@Consume |
| ⑤ 批处理时机 | 微任务(nextTick) | Fiber 调度 | Recomposer | RunLoop | next frame | Fiber | setData IPC | RenderLoop |
| ⑥ 生命周期解绑 | onUnmounted | useEffect cleanup | DisposableEffect | View 离开 | dispose() | useEffect cleanup | 页面 onUnload | aboutToDisappear |
这张表是本章最重要的"罗塞塔石碑"——后面 §4~§7 的所有内容,都是在这张表的某一列里展开细节。
# 4.Web 响应式设计
# 4.1 Vue 2 依赖收集
Vue 2(2014~2020 主流)的响应式核心是用 Object.defineProperty 把对象的每个属性改写成 getter/setter:
// Vue 2 响应式的最小实现(简化版)
function defineReactive(obj, key, val) {
const dep = new Dep(); // 每个 key 一个依赖收集器
Object.defineProperty(obj, key, {
get() {
if (Dep.target) { // ① 依赖收集
dep.depend(); // 当前正在执行的 Watcher 订阅这个 key
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // ② 通知所有 Watcher 重新执行
}
});
}
class Dep {
static target = null;
constructor() { this.subs = []; }
depend() { if (Dep.target) this.subs.push(Dep.target); }
notify() { this.subs.forEach(w => w.update()); }
}
class Watcher {
constructor(vm, expr, cb) {
this.cb = cb;
Dep.target = this; // ✨ 把自己设为"当前 target"
this.value = vm[expr]; // 执行表达式触发 getter,被 dep 收集
Dep.target = null;
}
update() { this.cb(); }
}
// 使用
const data = {};
defineReactive(data, 'count', 0);
new Watcher(data, 'count', () => console.log('count changed!'));
data.count = 1; // 自动触发 console.log
Vue 2 的两大致命缺陷:
- 无法监听新增/删除属性:
this.user.age = 18(user 没有 age 属性时)不会触发响应,必须用Vue.set(this.user, 'age', 18)或this.$set - 数组下标修改无效:
arr[0] = 'new'/arr.length = 0不触发,Vue 只能 hack 7 个数组方法(push/pop/shift/unshift/splice/sort/reverse)
这就是 Vue 3 必须迁移到 Proxy 的根本原因。
# 4.2 Vue 3 现代实现
Vue 3(2020 发布)用 ES6 Proxy 重写响应式,没有上面的两个缺陷:
// Vue 3 响应式核心(简化版)
let activeEffect = null;
const targetMap = new WeakMap(); // target -> Map<key, Set<effect>>
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
dep.add(activeEffect);
}
function trigger(target, key) {
const dep = targetMap.get(target)?.get(key);
dep?.forEach(effect => effect());
}
function reactive(target) {
return new Proxy(target, {
get(t, k, r) {
track(t, k); // ✨ 任意属性都拦截到
const result = Reflect.get(t, k, r);
return typeof result === 'object' ? reactive(result) : result;
// 深层对象懒包装
},
set(t, k, v, r) {
const ok = Reflect.set(t, k, v, r);
trigger(t, k);
return ok;
},
deleteProperty(t, k) { // ✨ 删除也响应
const ok = Reflect.deleteProperty(t, k);
trigger(t, k);
return ok;
}
});
}
function effect(fn) {
const wrapped = () => {
activeEffect = wrapped;
try { fn(); } finally { activeEffect = null; }
};
wrapped();
}
// 使用:跟 Vue 2 体验一致,但更强大
const state = reactive({ user: { name: 'Alice' } });
effect(() => console.log('hello', state.user.name));
state.user.name = 'Bob'; // ✓ 触发
state.user.age = 18; // ✓ 也触发(Vue 2 不行)
delete state.user.name; // ✓ 也触发
Vue 3 完整的响应式 API 家族:
import { ref, reactive, computed, watch, watchEffect, readonly, shallowRef } from 'vue';
// ref:基本类型的容器(需要 .value)
const count = ref(0);
count.value++;
// reactive:对象的代理
const state = reactive({ user: { name: 'Alice' }, todos: [] });
state.user.name = 'Bob';
// computed:派生数据,惰性求值 + 缓存
const doubled = computed(() => count.value * 2);
// watch:显式监听某个数据
watch(count, (newVal, oldVal) => { console.log(`${oldVal} → ${newVal}`); });
// watchEffect:自动追踪依赖
watchEffect(() => { console.log(state.user.name); });
// state.user.name 改变时自动重跑
// shallowRef:只代理顶层,深层不代理(性能优化)
const big = shallowRef({ huge: { tree: ... } });
Vue 3 vs Vue 2 的迁移痛点:
| 痛点 | Vue 2 | Vue 3 |
|---|---|---|
this.list[2] = x | 不响应 | 响应 |
this.obj.newKey = x | 不响应 | 响应 |
| 兼容 IE 11 | ✓ | ✗(Proxy 无法 polyfill) |
| TypeScript 类型推导 | 差 | 极佳(Composition API) |
# 4.3 React 差异化哲学
React 走了一条与 Vue 截然不同的路——它不做自动依赖收集,而是要求开发者显式声明 state 和依赖:
import { useState, useEffect, useMemo } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 显式 state
const [name, setName] = useState('Alice');
const doubled = useMemo(() => count * 2, [count]); // 显式依赖数组
useEffect(() => {
document.title = `${name} - ${count}`; // 显式 effect
return () => { /* cleanup */ };
}, [name, count]); // 显式声明依赖
return (
<div>
<p>{count} → {doubled}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
React 的核心三原则:
不可变性(Immutability):state 必须通过 setter 替换,不能就地修改
// ❌ React 看不见变化 const [user, setUser] = useState({ name: 'Alice' }); user.name = 'Bob'; // ✅ 必须创建新对象 setUser({ ...user, name: 'Bob' });UI = f(state):每次 state 变化,整个组件函数重新执行,产生新的 VDOM
Fiber 调度:VDOM diff 不是同步完成,可以中断、可以分优先级(紧急的输入响应 > 不紧急的列表渲染)
React 18 的并发特性:
import { useTransition, useDeferredValue } from 'react';
function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value); // 紧急:输入框立即响应
startTransition(() => {
setResults(searchHeavy(e.target.value)); // 非紧急:可被中断
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <ResultList />}
</>
);
}
React 19 Compiler(2024):React 团队意识到 useMemo / useCallback 写起来啰嗦,推出编译器自动 memo,让 React 接近 Vue 的开发体验。
# 4.4 细粒度三种路线
# 4.5 MobX OOP 风格
import { makeObservable, observable, computed, action } from 'mobx';
import { observer } from 'mobx-react';
class TodoStore {
@observable todos = [];
@computed get pending() {
return this.todos.filter(t => !t.done).length;
}
@action add(text) {
this.todos.push({ text, done: false });
}
}
const store = new TodoStore();
const TodoList = observer(() => (
<div>
<p>剩余 {store.pending}</p> {/* 自动订阅 store.pending */}
{store.todos.map(t => <Item key={t.text} t={t} />)}
</div>
));
特点:用 @observable / @observer 注解透明地响应,只重渲染真正用到该数据的组件,比 React 原生更精细。
# 4.6 Solid Signal 派
import { createSignal, createEffect, createMemo } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
createEffect(() => {
console.log('count is', count());
});
return (
<button onClick={() => setCount(count() + 1)}>
{count()} → {doubled()}
</button>
);
}
特点:没有 VDOM——count() 编译后直接绑定到具体 DOM 节点的文本,state 变化时只更新那一个文本节点,性能跑赢所有 VDOM 框架。
# 4.7 Svelte 编译响应式
<script>
let count = 0; // Svelte 5 写法:let count = $state(0);
$: doubled = count * 2; // $: 是编译时标记,表示这是 reactive statement
</script>
<button on:click={() => count++}>
{count} → {doubled}
</button>
特点:没有运行时——编译器把 count++ 改写成 count = count + 1; invalidate('count');,运行时极小(10KB),最终代码就是命令式 DOM 操作。
# 4.8 Web 派系对比
| 框架 | 收集模型 | 调度模型 | diff 模型 | 心智负担 |
|---|---|---|---|---|
| Vue 3 | Proxy 自动收集 | 微任务批处理 | VDOM | 低(写起来像普通对象) |
| React | 手动声明依赖 | Fiber 优先级调度 | VDOM | 中(要管 deps 数组) |
| MobX | autorun 自动收集 | 同步 + transaction | VDOM (React) | 低 |
| Solid | 细粒度 Signal | 同步 + 微任务 | 无 diff,直接绑 | 中(要写 () 调用) |
| Svelte | 编译时改写 | 异步队列 | 无运行时 diff | 极低 |
心智一句话:
- Vue 3 = "数据是普通对象,框架自动让它响应"
- React = "数据是值,setter 触发整树重渲,靠 diff 找差异"
- Solid = "数据是函数调用,订阅精确到节点"
- Svelte = "数据是变量,编译器把赋值改写成响应"
# 5.Android 响应式
Android 的响应式经历了清晰的四代演进:DataBinding(2015)→ LiveData(2017)→ Flow(2019)→ Compose(2021)。每一代都解决了上一代的痛点。
# 5.1 DataBinding 双向
DataBinding 是 Google 2015 推出的方案——用 XML 标签 + APT 编译期生成代码实现 MVVM 双向绑定:
<!-- activity_user.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User" />
</data>
<LinearLayout android:orientation="vertical">
<!-- 单向:user.name 变化时自动更新 TextView -->
<TextView android:text="@{user.name}" />
<!-- 双向:用户输入也会写回 user.name -->
<EditText android:text="@={user.name}" />
<!-- 表达式 -->
<TextView android:visibility="@{user.vip ? View.VISIBLE : View.GONE}" />
</LinearLayout>
</layout>
// User.kt 必须继承 BaseObservable
class User : BaseObservable() {
@get:Bindable
var name: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.name) // 手动通知(很啰嗦)
}
}
// Activity
val binding = DataBindingUtil.setContentView<ActivityUserBinding>(this, R.layout.activity_user)
binding.user = User().apply { name = "Alice" }
APT 编译期会生成 ActivityUserBindingImpl.java,里面是各种 setText、addTextChangedListener 的胶水代码。
DataBinding 的死亡之罪:
| 问题 | 描述 |
|---|---|
| XML 写逻辑 | @{user.vip ? VISIBLE : GONE} 调试地狱,IDE 支持差 |
BR.name 维护 | 字段一多就要写一堆 notifyPropertyChanged |
| 构建慢 | APT 生成大量 Java 文件,gradle 加 30s+ |
| 错误信息差 | XML 表达式出错栈在生成代码里,难定位 |
| 生命周期不感知 | 容易 setText 到已 destroy 的 View,需要配合 LiveData |
所以 Google 在 2017 推出 LiveData,2019 推出 ViewBinding(无逻辑版),DataBinding 沦为"历史项目维护工具"。
# 5.2 LiveData 观察者
LiveData 的核心创新:把"observer 的生命周期"作为一等公民,Activity 销毁时自动 remove,永远不会泄漏。
// ViewModel:业务层
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user // 暴露不可变接口
fun loadUser(id: String) {
viewModelScope.launch {
_user.value = userRepo.fetch(id) // 主线程
// 子线程用 _user.postValue(...)
}
}
}
// Activity:UI 层
class UserActivity : AppCompatActivity() {
private val vm: UserViewModel by viewModels()
override fun onCreate(s: Bundle?) {
super.onCreate(s)
// ✨ 关键:把 this(LifecycleOwner)传进去
vm.user.observe(this) { user ->
binding.tvName.text = user.name
// Activity 销毁后此 callback 自动不再触发,无需手动 remove
}
}
}
LiveData 的"四个保证":
| 保证 | 实现 |
|---|---|
| 生命周期感知 | 只在 STARTED/RESUMED 状态下触发 observer |
| 粘性事件 | 后来的 observer 会立刻收到最新值 |
| 不会重复触发 | value 没变(==)不通知 |
| 自动解绑 | LifecycleOwner.onDestroy 自动调用 removeObserver |
LiveData 的局限:
- 只能在主线程读取 .value(postValue 异步)
- 不支持复杂的流操作(map/filter/combine 要靠 Transformations,很弱)
- 粘性事件有时是坑:登录成功事件,用户进了首页再返回登录页又触发一次(用
SingleLiveEvent解决)
# 5.3 协程时代响应式
Kotlin Flow 是 RxJava 的"协程版替代品",更轻量、更原生:
import kotlinx.coroutines.flow.*
class UserViewModel : ViewModel() {
// StateFlow:替代 LiveData,更强大
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
// SharedFlow:一次性事件(Toast、Navigation)
private val _toast = MutableSharedFlow<String>()
val toast: SharedFlow<String> = _toast.asSharedFlow()
// 派生数据:用 flow 操作符组合
val displayName: StateFlow<String> = _user
.map { it?.name ?: "Guest" }
.stateIn(viewModelScope, SharingStarted.Eagerly, "Guest")
fun login(name: String) {
viewModelScope.launch {
_user.value = User(name = name)
_toast.emit("登录成功")
}
}
}
// Activity
class LoginActivity : AppCompatActivity() {
private val vm: UserViewModel by viewModels()
override fun onCreate(s: Bundle?) {
super.onCreate(s)
// 现代姿势:repeatOnLifecycle,UI 不可见时自动取消订阅
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { vm.displayName.collect { binding.tvName.text = it } }
launch { vm.toast.collect { Toast.makeText(this@LoginActivity, it, Toast.LENGTH_SHORT).show() } }
}
}
}
}
StateFlow vs SharedFlow vs LiveData 三者怎么选:
| 场景 | 选谁 | 理由 |
|---|---|---|
| 页面状态(用户信息、列表数据) | StateFlow | 有初始值、有粘性、value 可读 |
| 一次性事件(Toast、Navigation) | SharedFlow | 无粘性、不重放 |
| 遗留代码兼容 | LiveData | 现有 Activity Java 代码继续用 |
| 冷流 / 网络请求 | Flow | 有订阅才执行、可取消 |
Flow 操作符的强大:
// 搜索防抖 + 取消旧请求
val searchResults = queryFlow
.debounce(300) // 防抖 300ms
.distinctUntilChanged() // 去重
.flatMapLatest { query -> // 新输入到来自动取消老的请求
flow { emit(api.search(query)) }
}
.catch { emit(emptyList()) } // 错误兜底
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
# 5.4 Compose 状态系统
Compose 是 Google 的"声明式 UI 答卷",核心是 Snapshot 状态系统——一套类似 MVCC(多版本并发控制)的内存数据库:
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
var name by remember { mutableStateOf("Alice") }
// 派生数据
val doubled by remember { derivedStateOf { count * 2 } }
// 副作用:count 或 name 变化时执行
LaunchedEffect(count, name) {
analytics.track("count_changed", count, name)
}
Column {
Text("$name: $count → $doubled")
Button(onClick = { count++ }) { Text("+1") }
}
}
Compose 响应式的核心:Recomposition + Snapshot
1. 读取 state 时:Compose 记录"哪个 Composable 函数读了哪个 state"
2. 写入 state 时:Compose 标记所有读过这个 state 的 Composable 为"过时"
3. 下一帧 Recomposer:只重新执行被标记的 Composable,跳过其他
4. 智能 Skipping:参数没变的 Composable 直接跳过(@Stable / @Immutable 注解)
Snapshot 的高级用法(事务):
// 多个 state 变化是原子的
Snapshot.withMutableSnapshot {
user.name = "Bob"
user.age = 30
// 在 lambda 结束前,外面看到的还是旧值;结束才统一可见
}
Compose 状态的层次:
| 层次 | API | 用途 |
|---|---|---|
| 基本 state | mutableStateOf | 普通响应式变量 |
| 派生 state | derivedStateOf | 只在结果变化时触发重组(避免抖动) |
| 稳定集合 | mutableStateListOf | 列表的细粒度变化 |
| 跨重组保存 | remember | 重组时不重新创建 |
| 跨配置保存 | rememberSaveable | 屏幕旋转/进程恢复 |
| 跨 Composable 共享 | CompositionLocal | 类似 React Context |
| Flow 桥接 | collectAsState() | 把 StateFlow 转 Compose state |
经典坑:unstable 类型导致无效重组
// ❌ data class 中包含 List(默认 unstable),Compose 无法跳过
data class UserList(val users: List<User>)
@Composable
fun MyScreen(list: UserList) { ... } // 每次重组都会执行
// ✅ 用 ImmutableList 或 @Immutable 标记
@Immutable
data class UserList(val users: List<User>)
// 或用 kotlinx.collections.immutable 的 PersistentList
# 5.5 Android 派系总结
| 方案 | 时代 | 模型 | 推荐场景 |
|---|---|---|---|
| DataBinding | 2015 | XML + APT | 历史项目维护 |
| LiveData | 2017 | 生命周期感知 Observer | Java 项目 / 简单 MVVM |
| RxJava | 2014 | Observable 流 | 复杂的流处理(保留项目) |
| Kotlin Flow | 2019 | 协程 + 流 | 新 View 体系项目 |
| Compose State | 2021 | Snapshot + Recomposition | 新项目首选 |
一句话:Android 新项目 = Compose + StateFlow + ViewModel——StateFlow 管业务、Compose 管 UI、ViewModel 管生命周期。
# 6.iOS 响应式体系
iOS 也经历了清晰的四代演化:KVO(2003,Cocoa 原生)→ ReactiveCocoa / RxSwift(2012)→ Combine(2019)→ SwiftUI @State / @Observable(2019/2023)。
# 6.1 KVO 运行时魔法
KVO(Key-Value Observing)是 Cocoa 的原生响应式机制,靠 Runtime 在添加 observer 时动态生成子类实现:
// Person.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
// 注册监听
Person *p = [Person new];
[p addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
// 回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSLog(@"%@ changed: %@ → %@", keyPath, change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
}
p.name = @"Bob"; // 自动触发回调
// 必须手动移除(否则 dealloc 时崩溃)
[p removeObserver:self forKeyPath:@"name"];
KVO 的运行时魔法(揭秘):
[p addObserver: ...] 调用瞬间:
1. Runtime 动态创建 NSKVONotifying_Person 类(继承 Person)
2. 重写该子类的 setName: 方法,包装为:
[self willChangeValueForKey:@"name"];
[super setName:value];
[self didChangeValueForKey:@"name"]; ← 这里触发所有 observer
3. 把 p 的 isa 指针偷偷改成 NSKVONotifying_Person
4. p 的"类型"对外仍是 Person(重写 -class 方法欺骗)
KVO 的"七宗罪"(这是为什么后来必须用 Combine 替代):
| 痛点 | 描述 |
|---|---|
| ① 字符串 keyPath | @"name" 写错编译不报错,只能运行时崩溃(Swift 用 \.name 改善) |
| ② 手动移除 | 忘了 removeObserver → dealloc 时 crash |
| ③ 没有"组合"能力 | 无法 map/filter/combine 两个 keyPath |
| ④ 必须 NSObject 子类 | Swift 纯 struct/class 不能用 |
| ⑤ 多次注册混乱 | 同一个 observer 注册 2 次,回调走 2 次 |
| ⑥ 回调集中 | 所有 observer 回调挤在一个 observeValueForKeyPath 方法 |
| ⑦ 线程不明确 | 回调线程 = 写入线程,容易 UI 主线程冲突 |
# 6.2 RxSwift 流派
**RxSwift(2015)**把 Rx 的"一切皆流"思想搬到 iOS:
import RxSwift
import RxCocoa
class LoginViewController: UIViewController {
@IBOutlet var emailField: UITextField!
@IBOutlet var passwordField: UITextField!
@IBOutlet var loginButton: UIButton!
@IBOutlet var resultLabel: UILabel!
let disposeBag = DisposeBag() // 持有所有订阅
override func viewDidLoad() {
super.viewDidLoad()
// 邮箱有效 = 长度 > 5 且包含 @
let emailValid = emailField.rx.text.orEmpty
.map { $0.count > 5 && $0.contains("@") }
// 密码有效 = 长度 >= 6
let passwordValid = passwordField.rx.text.orEmpty
.map { $0.count >= 6 }
// 组合:两个都有效,按钮才能点
Observable.combineLatest(emailValid, passwordValid) { $0 && $1 }
.bind(to: loginButton.rx.isEnabled)
.disposed(by: disposeBag) // 自动管理生命周期
// 点击登录:节流防止双击
loginButton.rx.tap
.throttle(.seconds(1), scheduler: MainScheduler.instance)
.flatMapLatest { [weak self] _ -> Observable<String> in
guard let self = self else { return .empty() }
return self.api.login(self.emailField.text!, self.passwordField.text!)
}
.observe(on: MainScheduler.instance)
.bind(to: resultLabel.rx.text)
.disposed(by: disposeBag)
}
}
RxSwift 的核心抽象 = Observable / Subscriber / Operator / Scheduler / DisposeBag
优点:
- 用流的组合解决了 KVO 的所有痛点
- 学一套 Rx,跨 RxJava/RxJS/RxSwift 都能用
缺点:
- 学习曲线陡峭(200+ 操作符)
- 第三方库,包大小 +800KB
- 调试栈炸裂(一行报错栈深 30 层)
# 6.3 Combine 官方方案
Apple 在 WWDC 2019 推出 Combine——可以理解为"官方版 RxSwift + 语言级支持":
import Combine
import Foundation
class UserViewModel: ObservableObject {
@Published var name: String = "" // 自动产生 Publisher
@Published var age: Int = 0
@Published private(set) var isAdult: Bool = false
private var cancellables = Set<AnyCancellable>()
init() {
// 派生数据:age 变化时自动算 isAdult
$age
.map { $0 >= 18 }
.assign(to: &$isAdult)
// 监听 name 变化(带防抖)
$name
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { newName in
print("Name changed to \(newName)")
}
.store(in: &cancellables)
}
}
// ViewController
class MyVC: UIViewController {
let vm = UserViewModel()
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 订阅 isAdult,绑定到 UI
vm.$isAdult
.receive(on: DispatchQueue.main)
.sink { [weak self] adult in
self?.label.text = adult ? "成年" : "未成年"
}
.store(in: &cancellables)
}
}
Combine 的核心三角:
Publisher(发布者,相当于 Observable)
│ 提供:Output、Failure 两个关联类型
│ 操作符:map / filter / debounce / combineLatest / flatMap...
▼
Operator(中间变换)
│ 每个 operator 都是新的 Publisher
▼
Subscriber(订阅者)
│ .sink { ... } / .assign(to:) / 自定义 Subscriber
│ 返回 AnyCancellable
▼
Cancellable(生命周期)
│ .store(in: &cancellables)
│ Set 释放时自动取消所有订阅
Combine 的"语言级集成优势":
@Published注解自动让属性变成 PublisherObservableObject协议自动给 class 加objectWillChangePublisher- 与 SwiftUI 深度集成(
@StateObject/@ObservedObject直接订阅 ObservableObject) - 系统 API 内置 Publisher(
URLSession.dataTaskPublisher、NotificationCenter.publisher、Timer.publish)
# 6.4 SwiftUI 声明革命
SwiftUI(2019)把响应式做到了语言注解级别,写 UI 不再有任何 setText 调用:
import SwiftUI
struct CounterView: View {
@State private var count = 0 // 局部状态
@State private var name = "Alice"
var doubled: Int { count * 2 } // 派生(普通 computed property)
var body: some View {
VStack {
Text("\(name): \(count) → \(doubled)")
TextField("Name", text: $name) // ✨ $ 表示传双向绑定
Button("加 1") { count += 1 }
}
.onChange(of: count) { newValue in // 监听
print("count → \(newValue)")
}
}
}
SwiftUI 的状态属性家族:
| 注解 | 作用 | 谁拥有 | 何时失效 |
|---|---|---|---|
@State | 局部状态 | View 本身 | View 销毁 |
@Binding | 双向绑定(父传子) | 父 View | 父消失 |
@StateObject | 创建 ObservableObject | 当前 View(lazy 初始化) | View 销毁 |
@ObservedObject | 引用外部 ObservableObject | 外部 | 外部释放 |
@EnvironmentObject | 全 App 共享 | 环境 | App 退出 |
@AppStorage | UserDefaults 包装 | 系统 | 卸载 |
@SceneStorage | 场景状态恢复 | 系统 | 场景销毁 |
iOS 17 新出的 @Observable 宏(推荐新写法):
import Observation
// 不再需要 ObservableObject 协议、@Published
@Observable
class UserModel {
var name: String = ""
var age: Int = 0
}
struct UserView: View {
@Bindable var user: UserModel // 自动产生双向绑定
var body: some View {
VStack {
TextField("Name", text: $user.name)
Text("Age: \(user.age)")
}
}
}
@Observable 用 Swift Macro 在编译期生成响应式代码,比 Combine 更轻量、更细粒度(只重渲访问了变化属性的 View)。
# 6.5 iOS 派系总结
| 方案 | 时代 | 模型 | 推荐场景 |
|---|---|---|---|
| KVO | 2003 | Runtime swizzle | 系统 API 必须用(如 WKWebView 的 estimatedProgress) |
| ReactiveCocoa | 2012 | Rx 风格 | 历史项目 |
| RxSwift | 2015 | Rx 风格 | 跨平台团队(与 RxJava 共享思路) |
| Combine | 2019 | 官方流 | UIKit 项目 + iOS 13+ |
| SwiftUI @State | 2019 | 声明式 | iOS 14+ 新项目(部分场景) |
| @Observable | 2023 | 宏 + 细粒度 | iOS 17+ 新项目首选 |
一句话:iOS 新项目 = SwiftUI + @Observable——和 Android 的 Compose + StateFlow 异曲同工。
# 7.跨端响应式设计
# 7.1 Flutter 三层方案
Flutter 的响应式分三个层次——从原始到工程化:
# 层次 1:原始 setState(命令式)
class CounterPage extends StatefulWidget {
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
Widget build(BuildContext context) {
return Column(children: [
Text('$count'),
ElevatedButton(
onPressed: () => setState(() { count++; }), // ✨ 整个 State 重建
child: Text('+1'),
),
]);
}
}
问题:setState 重建整个 build 方法,子树虽然有 Element 复用,但仍偏粗粒度。
# 层次 2:ValueNotifier + ValueListenableBuilder(细粒度)
class CounterPage extends StatelessWidget {
final ValueNotifier<int> count = ValueNotifier(0);
Widget build(BuildContext context) {
return Column(children: [
ValueListenableBuilder<int>(
valueListenable: count,
builder: (context, value, child) => Text('$value'), // ✨ 只这里重建
),
ElevatedButton(
onPressed: () => count.value++,
child: Text('+1'),
),
]);
}
}
优点:只有 Builder 内重建,外面的 Column / Button 不动。
# 层次 3:Riverpod / Provider(生产级状态管理)
// 定义 Provider
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
// 使用
class CounterPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // 自动订阅
return Column(children: [
Text('$count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('+1'),
),
]);
}
}
Riverpod 的核心优势:
- 自动依赖注入,不再传递 BuildContext
- 编译期类型安全
- 支持异步 Provider(
FutureProvider、StreamProvider) - DevTool 完整可视化
# Flutter 三选一决策
| 场景 | 选谁 |
|---|---|
| 简单页面 / 短期 demo | setState |
| 单一变量 + 多处订阅 | ValueNotifier |
| 跨页面共享 / 复杂状态 | Riverpod / Bloc |
# 7.2 React Native 体系
RN 的响应式 = React 那套,原封不动搬到原生:
import React, { useState, useEffect } from 'react';
import { View, Text, Button, FlatList } from 'react-native';
function TodoListScreen() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
api.fetchTodos().then(data => {
setTodos(data);
setLoading(false);
});
}, []);
return (
<View>
{loading ? <Text>Loading...</Text> : (
<FlatList
data={todos}
keyExtractor={item => item.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
/>
)}
</View>
);
}
RN 特有的注意点:
- 跨 JS 桥:state 变化触发 VDOM diff,patches 通过 Bridge 序列化传到原生层——大列表频繁 setState 会卡 Bridge
- 新架构 Fabric:JSI 直接同步调用,跨语言通信成本骤降
- 状态管理首选:Redux Toolkit / Zustand / Jotai(与 Web React 共用)
# 7.3 小程序 setData
小程序的响应式 = setData 一个 API 走天下,但它的代价远大于 Web/Native:
// pages/index/index.js
Page({
data: {
count: 0,
userName: 'Alice',
list: []
},
onPlus() {
this.setData({ count: this.data.count + 1 });
// ⚠️ 此调用会:
// 1. 序列化新数据为 JSON 字符串
// 2. 通过 IPC 发到渲染线程(webview)
// 3. 渲染线程做脏检查,找出变化的节点
// 4. 触发 WXML 的重新渲染
},
onLoad() {
api.fetchList().then(list => {
this.setData({ list }); // 大列表会卡顿
});
}
});
小程序响应式的核心痛点:
| 痛点 | 描述 | 解法 |
|---|---|---|
| 跨进程 IPC | 逻辑层 ≠ 渲染层,setData 必须序列化 | 减少 setData 频率,合并调用 |
| 不能直接改 data | this.data.count++ 不触发 | 必须用 setData |
| 数据量限制 | 单次 setData 建议 < 256KB | 分页 / 增量更新 |
| 路径式 setData | 整个对象传过去太大 | 用 'list[0].name': 'x' 这种路径式更新 |
| diff 是脏检查 | 框架做整树 JSON diff,性能弱 | 拆小组件 |
小程序的现代解法(MobX-miniprogram):
import { observable, action } from 'mobx-miniprogram';
import { createStoreBindings } from 'mobx-miniprogram-bindings';
const store = observable({
count: 0,
increment: action(function() { this.count++; })
});
Page({
onLoad() {
this.storeBindings = createStoreBindings(this, {
store,
fields: ['count'],
actions: ['increment']
});
},
onUnload() {
this.storeBindings.destroyStoreBindings();
},
onTap() { this.increment(); } // 直接调用,自动 setData
});
# 7.4 鸿蒙 ArkTS 装饰器
鸿蒙 ArkTS 是 TypeScript 的扩展方言,响应式做成编译期装饰器:
@Entry
@Component
struct CounterPage {
@State count: number = 0 // 局部响应式
@State userName: string = 'Alice'
build() {
Column() {
Text(`${this.userName}: ${this.count}`)
.fontSize(20)
Button('+1')
.onClick(() => { this.count++ }) // 直接修改触发重渲
}
}
}
// 父子组件双向绑定
@Component
struct Child {
@Link parentCount: number // 双向绑定父的 @State
build() {
Button('Child +1').onClick(() => { this.parentCount++ })
}
}
@Entry
@Component
struct Parent {
@State count: number = 0
build() {
Column() {
Text(`${this.count}`)
Child({ parentCount: $count }) // $ 传递 @Link
}
}
}
// 跨层级共享(类似 Context)
@Component
struct Ancestor {
@Provide theme: string = 'dark'
build() { Descendant() }
}
@Component
struct Descendant {
@Consume theme: string // 自动接收祖先的 @Provide
build() { Text(this.theme) }
}
ArkTS 装饰器全家桶:
| 装饰器 | 用途 |
|---|---|
@State | 组件私有响应式状态 |
@Prop | 父→子单向传递(子改不影响父) |
@Link | 父↔子双向绑定 |
@Provide / @Consume | 跨层级(祖先↔后代)共享 |
@Observed / @ObjectLink | 嵌套对象的响应式 |
@Watch | 监听变化触发副作用 |
@Computed | 派生数据 |
@StorageLink | 与系统 PersistentStorage 双向同步 |
ArkTS 的设计哲学 = SwiftUI + Vue 装饰器的结合——既有声明式 UI,又有装饰器的可读性。
# 7.5 跨端响应式总结
| 框架 | 模型 | 跨端范围 |
|---|---|---|
| Flutter | Widget Tree + Element 复用 | Android/iOS/Web/Desktop |
| RN | React VDOM + Bridge | Android/iOS |
| Compose Multiplatform | Snapshot + Skia | Android/iOS/Desktop/Web (alpha) |
| 小程序 | setData + IPC + 脏检查 | 微信/支付宝/抖音 etc |
| 鸿蒙 ArkTS | 装饰器 + Snapshot 类 | 鸿蒙 / OpenHarmony |
一个关键观察:所有跨端框架最终都选了"声明式 + Snapshot"路线——这条路是终局答案。
# 8.横向对比矩阵
# 8.1 响应式能力对齐
把八个主流端在九个维度上做横向对照——这是本章的"全景索引":
| 维度 \ 端 | Vue 3 | React 19 | Compose | SwiftUI | Flutter (Riverpod) | RN | 小程序 | ArkTS |
|---|---|---|---|---|---|---|---|---|
| 依赖收集 | Proxy 自动 | 手动 deps | Snapshot 自动 | 编译期/Macro | ref.watch 自动 | 手动 deps | 整树脏检查 | 编译期装饰器 |
| 状态声明 | ref() | useState | mutableStateOf | @State/@Observable | StateNotifier | useState | data: {} | @State |
| 派生数据 | computed | useMemo | derivedStateOf | computed property | Provider 组合 | useMemo | WXS computed | @Computed |
| 副作用 | watchEffect | useEffect | LaunchedEffect | .onChange | ref.listen | useEffect | observers | @Watch |
| 跨组件共享 | provide/inject/Pinia | Context/Redux | CompositionLocal | @EnvironmentObject | Provider | Context | globalData/Mobx | @Provide/@Consume |
| 批处理 | 微任务 nextTick | Fiber 调度 | Recomposer/Frame | RunLoop | next frame | Fiber | setData IPC | RenderLoop |
| diff 模型 | VDOM | VDOM | Snapshot diff | View 等价比较 | Element tree | VDOM | 脏对象 diff | Snapshot 类 |
| 生命周期解绑 | onUnmounted | useEffect cleanup | DisposableEffect | View 离开 | dispose | useEffect cleanup | onUnload | aboutToDisappear |
| 状态持久化 | Pinia plugin | redux-persist | rememberSaveable + DataStore | @AppStorage/@SceneStorage | hydrated_bloc | AsyncStorage | wx.setStorage | PersistentStorage |
# 8.2 设计哲学站位
两个根本性轴线:
▲ 细粒度(节点级)
│
Solid │ SwiftUI @Observable
● │ ●
MobX ● │ ● Compose
│ ● Vue 3 (Proxy)
│
◄────────────────┼──────────────────►
编译时(零运行时) │ 运行时(代理魔法)
│
Svelte 5 │ Vue 2 (defineProperty)
● │ ●
Vue Vapor │ ● RxJS / Combine
● │
│ React 16 (VDOM)
│ ●
│ LiveData
│ ●
▼ 粗粒度(组件级 / 整树)
- 越靠右:运行时越重(更多内存、更慢启动)但灵活
- 越靠左:编译期完成更多工作,运行时极快
- 越靠上:更新越精准(只动变化的节点)
- 越靠下:开发者心智越简单(整树重算 + diff)
当前业界共识:往**"右上角"(细粒度运行时)和"左上角"(编译时细粒度)**两个方向同时演化——它们都在淘汰"左下"(粗粒度)的旧模型。
# 9.反模式与陷阱
# 9.1 渲染中含副作用
症状:在 build/render 函数中读取/修改外部状态,导致死循环或数据竞争。
// ❌ React 反例
function UserList() {
const [list, setList] = useState([]);
// 💀 render 中直接调 setList,触发 re-render,又调 setList,无限循环
fetch('/api/users').then(setList);
return <div>{list.map(...)}</div>;
}
// ✅ 正例:副作用放进 useEffect
function UserList() {
const [list, setList] = useState([]);
useEffect(() => {
fetch('/api/users').then(setList);
}, []); // 空依赖:只跑一次
return <div>{list.map(...)}</div>;
}
// ❌ Flutter 反例
Widget build(BuildContext context) {
fetchData(); // 💀 每次 build 都拉取数据
return Text(data);
}
// ✅ initState 中拉一次
void initState() {
super.initState();
fetchData().then((d) => setState(() => data = d));
}
根本原因:render/build 必须是纯函数——给同样的 state 必须产出同样的 UI,不能有任何副作用。响应式系统假设它可以被任意多次调用、可以被跳过,副作用会破坏这个契约。
# 9.2 直改不可变数据
症状:直接 mutate state,框架检测不到变化。
// ❌ React:push 没有返回新数组,引用没变
const [todos, setTodos] = useState([]);
function addTodo(item) {
todos.push(item); // 💀 引用相同,React 跳过 re-render
setTodos(todos);
}
// ✅ 创建新数组
function addTodo(item) {
setTodos([...todos, item]);
}
// ❌ Compose:MutableList 修改不触发重组
val list = remember { mutableListOf<String>() }
list.add("a") // 💀 Compose 不知道
// ✅ 用 mutableStateListOf
val list = remember { mutableStateListOf<String>() }
list.add("a") // ✓ 自动重组
根本原因:基于"引用相等"做 diff 的框架(React、Compose 部分场景)必须看到新引用才会重渲;可变引用就地修改 = 数据变了但引用没变 = 框架无视。
# 9.3 依赖循环无限渲染
// ❌ 死循环
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 💀 count 变 → effect 重跑 → count 又变 ...
}, [count]);
return <div>{count}</div>;
}
// ✅ 要么去掉 deps,要么用 setter 函数式更新
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
// ❌ Vue
watch(count, (v) => {
count.value = v * 2; // 💀 自己改自己
});
// ✅ 用 computed
const doubled = computed(() => count.value * 2);
根本原因:响应式系统是一个有向图,effect 同时是某个数据的订阅者和另一个数据的发布者就形成环。框架一般会做"递归深度限制"(Vue 3 是 100 次),但本质上是逻辑错误。
# 9.4 订阅未解内存泄漏
// ❌ Android 反例
class MyFragment : Fragment() {
override fun onCreateView(...) {
viewModel.userFlow.collect { // 💀 在错误的 scope,Fragment 销毁不取消
tvName.text = it.name // 持有 view 引用,view 被泄漏
}
}
}
// ✅ 用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userFlow.collect { binding?.tvName?.text = it.name }
}
}
}
// ❌ iOS 反例
class MyVC: UIViewController {
override func viewDidLoad() {
publisher.sink { ... } // 💀 没存 cancellable,立刻被释放(或永远不释放,看实现)
}
}
// ✅ 存进 cancellables
publisher.sink { ... }.store(in: &cancellables)
根本原因:订阅 = subject → observer 的强引用链。observer 持有外部资源(View / Activity / VC),如果 subject 是单例或长生命周期对象,整条链都泄漏。
# 9.5 滥用全局响应式
症状:把所有数据塞进全局 Store(Redux/Pinia/MobX/Riverpod 全局 Provider),导致:
- 每次任意 state 变化都要 diff 整个 Store
- 组件耦合度高,难拆出去复用
- 测试要 mock 整个 Store
// ❌ 反例:连临时输入都进全局
store.userInputDraft = '...'; // 💀 全局 store 被高频写
// ✅ 临时态留在组件本地
const [draft, setDraft] = useState('');
心智:状态有"作用域层级"——局部态用 useState/@State,跨组件用 Context/Provider,跨页面用 Store,跨 App 用 Storage——按需选择,不要"一把梭"。
# 9.6 反模式速查表
| 反模式 | 症状 | 根因 | 解法 |
|---|---|---|---|
| render 副作用 | 死循环 / 数据竞争 | 破坏纯函数 | 用 useEffect/LaunchedEffect |
| 就地修改 | UI 不更新 | 引用没变 | 创建新对象 / 用 stateOf |
| 依赖循环 | 栈溢出 / 卡死 | effect 互相触发 | 用 computed / 重新设计 |
| 订阅未解 | 内存泄漏 | 强引用链 | LifecycleOwner / disposeBag |
| 全局滥用 | 性能差 / 难测试 | 状态范围错 | 分层(本地→共享→全局) |
# 10.现代演进路线
# 10.1 Signal 与 VDom
2020 年后,响应式社区分裂成两大阵营:
VDOM 派(传统) Signal 派(新派)
───────────── ─────────────
React, Vue, RN Solid, Qwik, Preact Signal
↓ ↓
state 变 → 整树重算 → diff → patch state 变 → 直接修改订阅的 DOM 节点
↓ ↓
开发心智简单 性能极致
内存开销大 开发心智复杂(要写 `count()`)
两派的"中间地带":
- SwiftUI / Compose 用 Snapshot 做"伪 VDOM",但 Skipping 机制接近 Signal 的精度
- Svelte 5 用 Rune 把 Signal 的 API 通过编译器隐藏,看起来像 VDOM 实际是 Signal
# 10.2 编译时响应式
核心思路:把"运行时 diff"做的事情,搬到"编译期"完成。
<!-- Svelte 源码 -->
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
{count}
</button>
// Svelte 编译产物(简化)
function Counter($$anchor) {
let count = state(0);
const button = createElement('button');
button.onclick = () => set(count, get(count) + 1);
const text = createTextNode();
button.appendChild(text);
// ✨ 编译器知道 button 文本只依赖 count,生成精准更新
effect(() => { text.data = get(count); });
return button;
}
Vue 3.4+ 的 Vapor Mode(实验中)——同样思路:编译时把 <template> 中的响应式表达式直接绑到 DOM 节点,完全跳过 VDOM。预计运行时大小 < 10KB,性能跑赢 Solid。
React 19 Compiler 也是这条路——但走的是"自动 memo"而非"消灭 VDOM"。
# 10.3 时间旅行调试
响应式 + 单向数据流 + 不可变 = 可以记录每次 state 变化的快照,回放、回退、跳跃。
// Redux DevTools 的核心能力
[14:23:01] ADD_TODO { text: "Learn Redux" } state: { todos: [{...}] }
[14:23:05] TOGGLE { id: 1 } state: { todos: [{..., done:true}] }
[14:23:10] DELETE { id: 1 } state: { todos: [] }
→ 点击"回退到 14:23:05",整个 App 状态瞬间恢复
→ 录制操作回放,做演示 / 复现 bug
状态机化:把"任意状态变化"约束为"有限的合法转换"——XState(JS)、Tea(Elm)、Bloc(Flutter)。
// XState 示例
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
states: {
idle: { on: { FETCH: 'loading' } },
loading: { on: { SUCCESS: 'success', FAILURE: 'failure' } },
success: { type: 'final' },
failure: { on: { RETRY: 'loading' } }
}
});
// 任何非法的状态转换(如 idle → success)直接被拒绝,UI 永远在"合法状态"
# 10.4 服务端与边缘
React Server Components(RSC):让组件能在服务器端运行,把 fetch 结果直接序列化到客户端,响应式跨越客户端/服务端边界:
// app/page.tsx (RSC)
export default async function Page() {
const data = await db.user.findMany(); // 直接访问数据库
return <UserList users={data} />; // 序列化到客户端
}
Qwik 的 Resumability:服务端渲染后,客户端"恢复"状态而不是"重新水合",零 JS 启动时间。
这些都是响应式的未来方向——把响应式系统从"浏览器内"扩展到"客户端 + CDN + 服务器"全栈。
# 11.心智与设计哲学
# 11.1 跨端术语对照
最终一张"罗塞塔石碑"——同一个概念在不同平台叫什么:
| 通用概念 | Vue | React | Compose | SwiftUI | Flutter | RN | 小程序 | ArkTS |
|---|---|---|---|---|---|---|---|---|
| 单值响应式 | ref | useState | mutableStateOf | @State | ValueNotifier | useState | data | @State |
| 对象响应式 | reactive | useState({}) | mutableStateOf({}) | @Observable class | ChangeNotifier | useState({}) | data 嵌套 | @Observed |
| 派生 | computed | useMemo | derivedStateOf | computed prop | select | useMemo | WXS | @Computed |
| 监听 | watch | useEffect | LaunchedEffect | .onChange | addListener | useEffect | observers | @Watch |
| 双向绑定 | v-model | 受控组件 | MutableState+ Lambda | $state | TextEditingController | 手写 | model: | @Link+ $ |
| 全局共享 | pinia | Redux/Context | CompositionLocal | @EnvironmentObject | Provider/Riverpod | Context | globalData/Mobx | @Provide/@Consume |
| 持久化 | Pinia plugin | redux-persist | DataStore | @AppStorage | hydrated_bloc | AsyncStorage | wx.setStorage | PersistentStorage |
| 取消订阅 | onUnmounted | cleanup 函数 | DisposableEffect | View 销毁 | dispose() | cleanup | onUnload | aboutToDisappear |
# 11.2 本卷章节呼应
5.9 章不是孤岛——它和本卷前后章节构成完整的"交互系统"心智链:
| 本章 §X | 呼应章节 | 关系 |
|---|---|---|
| §3 六件套 | 5.2 视图加载渲染 (opens new window) | 响应式触发的最终是视图重绘——measure/layout/draw 是被响应式调用的执行层 |
| §3.7 调度 | 5.3 图形渲染管线 (opens new window) | 调度合并多次更新到一帧,配合 VSync 保证 60FPS |
| §3.9 生命周期 | 5.7 组件生命周期 (opens new window) | LifecycleOwner 是响应式自动解绑的依赖 |
| §6.4 SwiftUI | 5.8 页面导航 (opens new window) | NavigationPath 本质就是响应式数组 |
| §3.3 通知 | 5.5 消息机制设计 (opens new window) | 通知最终走 Handler/RunLoop/Microtask 队列 |
| §9 反模式 | 4.x 内存的真相 | 订阅未解 = 强引用链 = OOM 根源 |
| §10 状态机 | 6.x 架构思想 | 响应式是 MVVM/MVI 的运行时基石 |
# 11.3 通用心智口诀
把整章浓缩成七句话,挂在显示器上:
第一句:UI = f(state)——视图永远是 state 的纯函数,没有第二条真相。
第二句:Single Source of Truth——一个数据只能有一个权威来源,多份就是 bug 温床。
第三句:Read 收集依赖,Write 触发通知——这是响应式的两个基本动作,所有 API 都建立在这两个动作之上。
第四句:Mutate 用不可变,引用要换新——可变就地修改是 diff 派框架的死敌。
第五句:副作用必须放进 effect——render/build 必须保持纯净,副作用是它的近义词「污染」。
第六句:订阅必须有归属——LifecycleOwner / disposeBag / cleanup 三件套,少一件就泄漏。
第七句:状态分层管理——本地态用
useState/@State,共享用Context/Provider,跨页用Store,永久用Storage,按需选择不一把梭。
至此 5.9 完结。