← 全部文章

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 推断。