Vue 原理:响应式编程(一)
这篇是 2024 年读 Vue 响应式系统前整理的概念笔记。先把几种编程范式放在一起比较,再看 Vue 如何传播状态变化。
编程范式
编程范式可以理解为组织程序和描述问题的方式。下面这些分类会有交集,不必把它们看成互斥选项。
命令式编程
命令式编程描述一步步执行什么操作。C 是常见例子:通过赋值、if/else、for 等语句组织控制流,也会直接处理内存与输入输出。这里主要关注它“描述执行步骤”的特点。
面向对象编程
面向对象编程(OOP)用对象组织状态和行为,这里主要看封装、继承和多态。它们提供了组织代码的方式,是否便于维护还取决于职责划分。
C++ 和 Java 都支持面向对象编程,但语言的内存管理方式不一样。理解 OOP 时,先看类与对象如何组织职责,不把它和某一种内存管理或性能表现绑定。
声明式编程
声明式编程侧重描述想要的结果,把部分执行细节交给底层机制。它与命令式的区别可以先从“结果怎么表达”来看。
AOP(面向切面编程)是一种编程方式,用来把一些通用的功能(如日志记录、事务管理、安全性等)从业务逻辑中分离出来。通过定义切面(Aspect),这些功能可以独立实现,使代码更清晰和模块化。
IoC(控制反转)是一种设计原则,用来减少代码之间的耦合。通过把对象的创建和依赖关系的管理交给外部容器(如Spring),实现控制反转,使代码更灵活和可维护。
函数式编程
函数式编程强调用函数组织计算。这里关注两个相关但不同的概念:不可变数据,以及结果只取决于输入、不依赖外部可变状态的纯函数。
Java 8 引入了 lambda 表达式语法。为了理解函数的组合,这里先把函数看作输入到输出的映射,暂不展开 lambda 演算。
当输入和输出边界清楚时,函数就更容易单独组合和复用。
相比逐步控制执行过程,函数式写法更关注数据如何经过一系列转换。减少共享可变状态,可以减少并发时维护状态与处理冲突的负担;但不能仅凭采用函数式写法,就断言不再需要同步机制。
响应式编程 & Vue.js
响应式编程关注数据流和变化传播:数据变了,依赖它的计算或界面也随之更新。下面先从谁通知谁来理解这个过程。
观察者模式可以帮助理解响应式通知。先用简单的迭代器与订阅模型来比较:
在编程中,迭代器和响应式流是两种不同的数据处理方式。其主要区别在于它们的数据传递模式。迭代器基于“拉”模式,而响应式流基于“推”模式。
- “拉”模式意味着消费者(即使用数据的一方)主动请求数据。迭代器提供一个接口,允许消费者通过调用
next()方法逐个获取数据项。这种方式的优点是消费者可以完全控制数据的获取节奏,适用于需要逐步处理数据的场景。- 相反,“推”模式意味着生产者(即提供数据的一方)主动发送数据给消费者。响应式流通过订阅机制,允许消费者订阅数据流,当有新数据时,生产者会自动将数据推送给所有订阅者。这种方式的优点是可以实时处理数据更新,适用于需要响应数据变化的场景。
使用迭代器时,开发者通过 hasNext()、next() 这样的接口推进遍历。订阅模型则由发布者(Publisher)通知订阅者(Subscriber)。这里是入门对比,具体响应式系统还可能组合推与拉。
Vue 通过响应式系统追踪依赖,在状态变化后安排相关更新。下面看状态与视图如何联动。
例如,在 Vue.js 中,你可以定义一个响应式数据对象:
const app = Vue.createApp({
data() {
return {
message: 'Hello, Vue!'
}
}
})
当 message 的值改变时,Vue.js 会自动更新绑定到 message 的所有视图。这个绑定不需要开发者逐项手动修改 DOM。
从观察者模式来理解,这里是状态变化通知了依赖它的逻辑。具体通知方式,还要继续看响应式系统的实现。
几个相关机制
下面分别记录变化传播、不可变数据和订阅。它们经常一起被讨论,但不全是 Vue 响应式系统的设计要求。
- 变化传播:在组件中读取响应式属性时建立依赖;修改属性后,相关模板绑定会更新。
import { reactive } from 'vue'
const state = reactive({
count: 0
})
// 当 state.count 变化时,所有依赖 state.count 的地方都会自动更新
state.count++
- 不可变数据:在 Redux 这类状态管理方案中,更新时生成新对象,而不是直接修改旧对象。下面这个例子与上面的
state.count++是两种不同的更新方式,不能据此把不可变性当成 Vue 响应式的要求。
const state = {
count: 0
}
// 创建一个新的状态对象,而不是修改现有的状态对象
const newState = { ...state, count: state.count + 1 }
-
推送:生产者有新数据时,主动通知订阅者。
-
订阅:消费者登记需要处理的变化。下面用
watchEffect观察一个 ref:
import { ref, watchEffect } from 'vue'
const dataStream = ref(0)
// 订阅 dataStream,实现对数据的实时处理和更新
watchEffect(() => {
console.log('Data stream updated:', dataStream.value)
})
// 改变 dataStream 的值,触发更新
dataStream.value = 2
@vue/reactivity 库设计理念
读 @vue/reactivity 时,我先关注数据对象、依赖追踪和变化通知;组件视图的批量更新则需要结合 Vue 的其他部分来看。
-
响应式数据对象:
reactive将普通对象转换为响应式对象。watchEffect和computed用于组织依赖这些数据的副作用与计算。 -
依赖追踪:组件渲染过程中读取响应式属性时,Vue 记录依赖关系。之后属性变化,就能找到对应的更新逻辑。
-
变化通知:属性变化后,通知依赖它的副作用。需要继续读源码的问题是:依赖如何保存、变化时通知哪些订阅者?
-
批量与异步更新:组件视图更新会经过调度,连续变化可以合并处理。具体时机要结合组件调度来看,不能只从
@vue/reactivity推断。