编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 序卷方法论

  • 数据的本质

  • 运行时模型

  • 并发的设计

  • 内存的真相

  • 交互和系统

    • README
    • 1.窗口核心设计思想
    • 2.视图加载渲染设计
    • 3.图形渲染管线原理
    • 4.手势事件设计灵魂
    • 5.消息机制设计思想
    • 6.跨进程的通信设计
    • 7.组件生命周期管理
    • 8.页面导航与路由设计
    • 9.响应式数据绑定设计
      • 目录
      • 1.数据不同步事故
        • 1.1 真实事故案例
        • 1.2 表面与根因修复
        • 1.3 事故核心启示
      • 2.为什么需要响应式
        • 2.1 命令式三宗罪
        • 2.2 散弹枪式修改
        • 2.3 派生数据陈旧
        • 2.4 多源数据打架
        • 2.5 观察者到响应式
        • 2.6 响应式设计目标
      • 3.响应式六件套契约
        • 3.1 可观察数据源
        • 3.2 依赖收集机制
        • 3.3 变更通知机制
        • 3.4 同步还是异步
        • 3.5 父子通知顺序
        • 3.6 循环依赖处理
        • 3.7 视图调度批量
        • 3.8 差异比对算法
        • 3.9 生命周期绑定
        • 3.10 跨端名词矩阵
      • 4.Web 响应式设计
        • 4.1 Vue 2 依赖收集
        • 4.2 Vue 3 现代实现
        • 4.3 React 差异化哲学
        • 4.4 细粒度三种路线
        • 4.5 MobX OOP 风格
        • 4.6 Solid Signal 派
        • 4.7 Svelte 编译响应式
        • 4.8 Web 派系对比
      • 5.Android 响应式
        • 5.1 DataBinding 双向
        • 5.2 LiveData 观察者
        • 5.3 协程时代响应式
        • 5.4 Compose 状态系统
        • 5.5 Android 派系总结
      • 6.iOS 响应式体系
        • 6.1 KVO 运行时魔法
        • 6.2 RxSwift 流派
        • 6.3 Combine 官方方案
        • 6.4 SwiftUI 声明革命
        • 6.5 iOS 派系总结
      • 7.跨端响应式设计
        • 7.1 Flutter 三层方案
        • 7.2 React Native 体系
        • 7.3 小程序 setData
        • 7.4 鸿蒙 ArkTS 装饰器
        • 7.5 跨端响应式总结
      • 8.横向对比矩阵
        • 8.1 响应式能力对齐
        • 8.2 设计哲学站位
      • 9.反模式与陷阱
        • 9.1 渲染中含副作用
        • 9.2 直改不可变数据
        • 9.3 依赖循环无限渲染
        • 9.4 订阅未解内存泄漏
        • 9.5 滥用全局响应式
        • 9.6 反模式速查表
      • 10.现代演进路线
        • 10.1 Signal 与 VDom
        • 10.2 编译时响应式
        • 10.3 时间旅行调试
        • 10.4 服务端与边缘
      • 11.心智与设计哲学
        • 11.1 跨端术语对照
        • 11.2 本卷章节呼应
        • 11.3 通用心智口诀
    • 10.国际化适配的设计
  • 内功
  • 交互和系统
杨充
2026-06-28
目录

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.数据不同步事故
    • 1.1 真实事故案例
    • 1.2 表面与根因修复
    • 1.3 事故核心启示
  • 2.为什么需要响应式
    • 2.1 命令式三宗罪
    • 2.2 散弹枪式修改
    • 2.3 派生数据陈旧
    • 2.4 多源数据打架
    • 2.5 观察者到响应式
    • 2.6 响应式设计目标
  • 3.响应式六件套契约
    • 3.1 可观察数据源
    • 3.2 依赖收集机制
    • 3.3 变更通知机制
    • 3.4 同步还是异步
    • 3.5 父子通知顺序
    • 3.6 循环依赖处理
    • 3.7 视图调度批量
    • 3.8 差异比对算法
    • 3.9 生命周期绑定
    • 3.10 跨端名词矩阵
  • 4.Web 响应式设计
    • 4.1 Vue 2 依赖收集
    • 4.2 Vue 3 现代实现
    • 4.3 React 差异化哲学
    • 4.4 细粒度三种路线
    • 4.5 MobX OOP 风格
    • 4.6 Solid Signal 派
    • 4.7 Svelte 编译响应式
    • 4.8 Web 派系对比
  • 5.Android 响应式
    • 5.1 DataBinding 双向
    • 5.2 LiveData 观察者
    • 5.3 协程时代响应式
    • 5.4 Compose 状态系统
    • 5.5 Android 派系总结
  • 6.iOS 响应式体系
    • 6.1 KVO 运行时魔法
    • 6.2 RxSwift 流派
    • 6.3 Combine 官方方案
    • 6.4 SwiftUI 声明革命
    • 6.5 iOS 派系总结
  • 7.跨端响应式设计
    • 7.1 Flutter 三层方案
    • 7.2 React Native 体系
    • 7.3 小程序 setData
    • 7.4 鸿蒙 ArkTS 装饰器
    • 7.5 跨端响应式总结
  • 8.横向对比矩阵
    • 8.1 响应式能力对齐
    • 8.2 设计哲学站位
  • 9.反模式与陷阱
    • 9.1 渲染中含副作用
    • 9.2 直改不可变数据
    • 9.3 依赖循环无限渲染
    • 9.4 订阅未解内存泄漏
    • 9.5 滥用全局响应式
    • 9.6 反模式速查表
  • 10.现代演进路线
    • 10.1 Signal 与 VDom
    • 10.2 编译时响应式
    • 10.3 时间旅行调试
    • 10.4 服务端与边缘
  • 11.心智与设计哲学
    • 11.1 跨端术语对照
    • 11.2 本卷章节呼应
    • 11.3 通用心智口诀

# 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 和这里同步
}

故障链:

  1. 用户点击 +,CartActivity.quantity 从 2 → 5,tvQuantity 和 tvTotal 都更新了
  2. 但 CartManager.quantity 没有被通知(两个数据源各活各的)
  3. 用户点结算 → intent.putExtra("price", CartManager.totalPrice) 传的是旧值 99×2 = 198
  4. 结算页显示 198,用户实付按 198 扣款;但订单系统按数据库 quantity=5 发了 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 的两大致命缺陷:

  1. 无法监听新增/删除属性:this.user.age = 18(user 没有 age 属性时)不会触发响应,必须用 Vue.set(this.user, 'age', 18) 或 this.$set
  2. 数组下标修改无效: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 的核心三原则:

  1. 不可变性(Immutability):state 必须通过 setter 替换,不能就地修改

    // ❌ React 看不见变化
    const [user, setUser] = useState({ name: 'Alice' });
    user.name = 'Bob';
    
    // ✅ 必须创建新对象
    setUser({ ...user, name: 'Bob' });
    
  2. UI = f(state):每次 state 变化,整个组件函数重新执行,产生新的 VDOM

  3. 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 编译响应式

&lt;script>
    let count = 0;          // Svelte 5 写法:let count = $state(0);
    $: doubled = count * 2; // $: 是编译时标记,表示这是 reactive statement
&lt;/script>

&lt;button on:click={() => count++}>
    {count} → {doubled}
&lt;/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 的局限:

  1. 只能在主线程读取 .value(postValue 异步)
  2. 不支持复杂的流操作(map/filter/combine 要靠 Transformations,很弱)
  3. 粘性事件有时是坑:登录成功事件,用户进了首页再返回登录页又触发一次(用 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 的"语言级集成优势":

  1. @Published 注解自动让属性变成 Publisher
  2. ObservableObject 协议自动给 class 加 objectWillChange Publisher
  3. 与 SwiftUI 深度集成(@StateObject / @ObservedObject 直接订阅 ObservableObject)
  4. 系统 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 {
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int count = 0;
  
  @override
  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);
  
  @override
  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 {
  @override
  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 特有的注意点:

  1. 跨 JS 桥:state 变化触发 VDOM diff,patches 通过 Bridge 序列化传到原生层——大列表频繁 setState 会卡 Bridge
  2. 新架构 Fabric:JSI 直接同步调用,跨语言通信成本骤降
  3. 状态管理首选: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 反例
@override
Widget build(BuildContext context) {
  fetchData();   // 💀 每次 build 都拉取数据
  return Text(data);
}

// ✅ initState 中拉一次
@override
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"做的事情,搬到"编译期"完成。

&lt;!-- Svelte 源码 -->
&lt;script>
    let count = $state(0);
&lt;/script>

&lt;button onclick={() => count++}>
    {count}
&lt;/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 完结。

上次更新: 2026/07/15, 11:23:11
8.页面导航与路由设计
10.国际化适配的设计

← 8.页面导航与路由设计 10.国际化适配的设计→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式