Vue.js 的核心特性之一是其响应式系统 (Reactivity System)。在 Vue 3 中,响应式系统经历了重大革新,从 Vue 2 基于 Object.defineProperty 的实现全面升级为基于 JavaScript Proxy。这一转变解决了 Vue 2 中存在的诸多限制,如无法检测对象属性的添加/删除、无法有效监听数组变动等,并为 Composition API (组合式 API) 提供了坚实的基础。深入理解 Vue 3 的响应式原理,对于编写高效、可维护的 Vue 应用至关重要。

核心思想:
Vue 3 的响应式系统借助 Proxy 对象劫持数据对象的读取 (get) 和修改 (set) 操作,并在副作用函数 (Effect Function,如组件渲染函数、计算属性、侦听器) 执行时收集其依赖 (track)。当响应式数据发生变化时,系统会通知所有依赖于该数据的副作用函数重新执行 (trigger),从而实现数据的自动更新到 UI。


一、响应式系统概述

响应式系统是一个能够自动追踪数据变化并作出相应更新的机制。在 Vue 中,当数据变化时,视图会自动更新,这就是响应式系统在幕后工作的结果。

Vue 3 响应式系统的几个关键概念包括:

  • 响应式数据 (Reactive Data):通过 reactive()ref() 创建的数据,其读写操作会被代理。
  • 副作用函数 (Effect Function):依赖于响应式数据的函数。当响应式数据变化时,这些函数会被重新执行。最常见的副作用是组件的渲染函数。
  • 依赖追踪 (Dependency Tracking):在副作用函数执行期间,系统会记录该函数访问了哪些响应式数据。
  • 派发更新 (Triggering Updates):当响应式数据发生修改时,系统会查找所有依赖于该数据的副作用函数并重新执行它们。

二、从 Object.definePropertyProxy

Vue 3 响应式系统的核心升级在于从 Object.defineProperty 切换到 Proxy。理解这一转变是理解 Vue 3 响应式的关键。

2.1 Vue 2 (Object.defineProperty) 的局限性

在 Vue 2 中,响应式是基于 Object.defineProperty 实现的。它通过遍历对象的所有属性,为每个属性定义 gettersetter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Vue 2 模拟
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
if (typeof val === 'object' && val !== null) {
observe(val);
}

let dep = new Dep(); // 每个属性一个Dep实例,用于收集依赖

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集:当属性被读取时,将当前的Effect函数添加到Dep中
if (Dep.target) {
dep.add(Dep.target);
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 新值如果是对象,需要再次observe
if (typeof newVal === 'object' && newVal !== null) {
observe(newVal);
}
// 派发更新:当属性被修改时,通知Dep中所有依赖重新执行
dep.notify();
}
});
}

function observe(value) {
if (typeof value !== 'object' || value === null) {
return value;
}
// 遍历所有属性,转换为响应式
Object.keys(value).forEach(key => defineReactive(value, key, value[key]));
}

Object.defineProperty 的主要限制:

  1. 无法检测对象属性的添加或删除defineProperty 只能劫持对象已存在的属性。当你向一个响应式对象添加新属性时,Vue 2 无法检测到,也不会触发视图更新。需要使用 Vue.setthis.$set
  2. 无法检测数组索引和长度的变化:Vue 2 通过重写数组原型上的方法(如 push, pop, splice 等)来解决数组的响应式问题,但对于直接通过索引修改数组元素(如 arr[0] = newValue)或修改 length 属性,仍然无法检测。
  3. 深层嵌套对象性能开销大:初始化时需要递归遍历所有嵌套属性,如果对象层级很深,会存在较大的性能开销。

2.2 Vue 3 (Proxy) 的优势

Proxy 是 ES6 引入的新特性,它允许你创建一个对象的代理,拦截几乎所有的基本操作,如属性查找、赋值、枚举、函数调用等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Vue 3 核心思想模拟
function createReactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}

const proxy = new Proxy(target, {
get(target, key, receiver) {
// 依赖收集:当属性被读取时,记录当前是哪个Effect在读取
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
// Proxy 可以检测到属性的添加或删除
const result = Reflect.set(target, key, value, receiver);
if (result && value !== oldValue) { // 只有值真正改变才触发
// 派发更新:通知所有依赖于该属性的Effect重新执行
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
// Proxy 可以检测到属性的删除
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (result && hadKey) {
// 派发更新
trigger(target, key);
}
return result;
}
// 更多拦截器...
});

return proxy;
}

Proxy 的主要优势:

  1. 全面拦截:可以拦截 13 种对象操作,包括 getsetdeletePropertyhasownKeys 等,这意味着可以原生支持对象属性的添加/删除、数组元素的修改和长度的变化。
  2. 惰性代理 (Lazy Proxy):Proxy 只代理对象本身,并不会在初始化时递归遍历所有深层嵌套属性。它只会在访问到嵌套属性时才进行代理,这大大减少了初始化的性能开销。
  3. 更简洁的 API:无需像 Object.defineProperty 那样为每个属性单独配置 gettersetter

三、Vue 3 响应式核心 API

Vue 3 提供了 Composition API 来创建和管理响应式状态。

3.1 reactive()

reactive() 函数接收一个对象作为参数,并返回该对象的一个响应式代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { reactive } from 'vue';

const state = reactive({
count: 0,
user: {
name: 'Vue',
age: 3
},
items: ['apple', 'banana']
});

console.log(state.count); // 0

state.count++; // state.count 变为 1,视图更新
state.user.age = 4; // state.user.age 变为 4,视图更新
state.items.push('orange'); // 数组新增元素,视图更新
state.newProp = 'hello'; // 新增属性,视图更新 (Vue 2 无法检测)
delete state.user.name; // 删除属性,视图更新 (Vue 2 无法检测)

注意: reactive() 只能用于对象类型 (包括数组和 Map/Set)。对于原始值类型 (string, number, boolean, null, undefined, symbol),需要使用 ref()

3.2 ref()

ref() 函数接收一个内部值作为参数,并返回一个响应式且可变的 ref 对象。这个 ref 对象只有一个 .value 属性,用于访问或修改其内部值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref } from 'vue';

const count = ref(0);
const message = ref('Hello');
const isActive = ref(true);

console.log(count.value); // 0

count.value++; // count.value 变为 1,视图更新
message.value = 'World'; // message.value 变为 World,视图更新

// ref 也可以持有对象类型,它会自动通过 reactive 转换为响应式
const user = ref({ name: 'Bob', age: 25 });
user.value.age++; // user.value.age 变为 26,视图更新

在模板中,当 ref 是顶层属性时,Vue 会自动解套(无需 .value)。但在 JavaScript 中,必须使用 .value

3.3 toRef() / toRefs()

  • toRef():为源响应式对象上的某个属性创建一个 ref。新的 ref 与其源属性保持同步,对 ref 的修改会影响源属性,反之亦然。这在需要将一个响应式对象的某个属性作为 ref 传递给函数或组件时非常有用,可以避免失去响应性。
  • toRefs():将一个响应式对象的所有顶层属性转换为一系列 ref 对象。这在解构 reactive 对象时保持响应性非常有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { reactive, toRefs, toRef } from 'vue';

const user = reactive({
name: 'Alice',
age: 30
});

// toRefs 解构,解决直接解构会失去响应性的问题
const { name, age } = toRefs(user);
console.log(name.value); // Alice
name.value = 'Alicia';
console.log(user.name); // Alicia (user.name 也更新了)

// toRef 为特定属性创建 ref
const userAgeRef = toRef(user, 'age');
console.log(userAgeRef.value); // 30
userAgeRef.value++;
console.log(user.age); // 31 (user.age 也更新了)

3.4 computed()

computed() 创建一个计算属性。它接收一个 getter 函数,返回一个只读的响应式 ref。当其依赖的响应式数据变化时,getter 函数会重新执行,并更新计算属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { reactive, computed } from 'vue';

const prices = reactive({
unitPrice: 10,
quantity: 2
});

const totalPrice = computed(() => {
return prices.unitPrice * prices.quantity; // 依赖于 prices.unitPrice 和 prices.quantity
});

console.log(totalPrice.value); // 20

prices.quantity = 5; // 修改依赖
console.log(totalPrice.value); // 50 (自动更新)

3.5 watch() / watchEffect()

  • watch():精确侦听一个或多个响应式数据源,并在数据源变化时执行副作用函数。它允许你访问旧值和新值,并可以配置 immediatedeep 等选项。
  • watchEffect():立即执行一个函数,同时响应性地追踪其依赖。当函数中的任何响应式依赖发生变化时,它会重新运行。watchEffect 的优势在于它会自动收集依赖,无需手动指定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { ref, watch, watchEffect } from 'vue';

const count = ref(0);

// watch: 明确指定侦听源
watch(count, (newCount, oldCount) => {
console.log(`Count changed from ${oldCount} to ${newCount}`);
});

setTimeout(() => {
count.value = 1; // 触发 watch
}, 1000); // Output: Count changed from 0 to 1

// watchEffect: 自动收集依赖
watchEffect(() => {
console.log(`Current count is: ${count.value}`);
});
// 立即执行: Output: Current count is: 0
setTimeout(() => {
count.value = 2; // 触发 watchEffect
}, 2000); // Output: Current count is: 2

3.6 readonly()

readonly() 接收一个对象 (响应式或普通对象) 或 ref,返回一个使其只读的代理。对只读代理的任何修改尝试都会失败并抛出警告。

1
2
3
4
5
6
7
8
9
10
11
12
import { reactive, readonly } from 'vue';

const original = reactive({ count: 0 });
const copy = readonly(original);

console.log(copy.count); // 0

original.count++; // original 可以修改
console.log(original.count); // 1
console.log(copy.count); // 1 (copy 仍然反映 original 的最新状态)

// copy.count++; // 运行时会发出警告:Set operation on key "count" failed: target is readonly.

四、响应式系统内部机制深度解析

Vue 3 的响应式系统可以用下图来表示其核心工作流程:

4.1 track() (依赖收集)

当一个副作用函数 (例如组件的渲染函数、computedgetterwatchwatchEffect 回调) 执行时,它会成为“活跃的副作用”。此时,Vue 会有一个全局变量 activeEffect 指向这个函数。

当活跃的副作用函数访问一个响应式对象的属性时,Proxyget 拦截器会被触发,并调用 track() 函数。

track() 的核心逻辑是:

  1. 找到当前被访问的对象 target
  2. 找到被访问的属性 key
  3. 从全局存储中,找到与 targetkey 对应的依赖集合 (Dep Set)。
  4. 将当前的 activeEffect 添加进这个 Dep Set 中。

数据结构: Vue 内部使用 WeakMap<Target, Map<Key, Set<Effect>>> 来存储依赖关系。

  • targetMap (WeakMap): target -> depsMap
  • depsMap (Map): key -> dep (Set of Effects)
  • dep (Set): 存储所有依赖于该属性的Effect函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 伪代码
let activeEffect = null; // 当前正在执行的副作用函数

// targetMap: target -> depsMap (key -> dep)
const targetMap = new WeakMap();

function track(target, key) {
if (!activeEffect) return; // 如果没有活跃的Effect,则不收集

let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}

let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}

dep.add(activeEffect); // 将当前活跃Effect添加到依赖集合中
// 同时,Effect自身也记录它依赖了哪些dep,用于清理
activeEffect.deps.add(dep);
}

// 副作用函数包装器 (例如 watchEffect)
function effect(fn) {
const effectFn = function () {
// 每次执行前清理旧依赖
cleanup(effectFn);
activeEffect = effectFn; // 设置当前活跃Effect
const result = fn(); // 执行副作用函数,触发get,从而进行依赖收集
activeEffect = null; // 清理活跃Effect
return result;
};
effectFn.deps = new Set(); // 存储这个Effect所依赖的所有dep
effectFn(); // 立即执行一次
return effectFn;
}```

### 4.2 `trigger()` (派发更新)

当响应式数据被修改时,`Proxy` 的 `set` 拦截器会被触发,并调用 `trigger()` 函数。

`trigger()` 的核心逻辑是:
1. 找到被修改的对象 `target`。
2. 找到被修改的属性 `key`。
3. 从全局存储中,找到与 `target` 和 `key` 对应的依赖集合 (Dep Set)。
4. 遍历该 Dep Set 中的所有 Effect 函数,并执行它们。

```javascript
// 伪代码
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return; // 该对象没有依赖被收集

const dep = depsMap.get(key);
if (!dep) return; // 该属性没有依赖被收集

// 遍历并执行所有依赖 Effect
dep.forEach(effectFn => {
// 调度器:将Effect放入任务队列,实现批量更新
if (effectFn.scheduler) {
effectFn.scheduler(effectFn);
} else {
effectFn();
}
});
}

4.3 调度器 (Scheduler)

为了优化性能,Vue 3 的响应式系统引入了调度器 (Scheduler)。当多个响应式数据在短时间内被修改时,trigger() 会多次调用 Effect。如果不进行处理,会导致副作用函数(特别是组件渲染函数)频繁执行,造成性能浪费。

调度器的作用是将副作用函数的执行放入一个队列中,并在下一个事件循环的“微任务”阶段批量执行。这确保了:

  1. 每个 Effect 在一个更新周期内只执行一次。
  2. DOM 更新是异步和批量的。

例如,在修改多个 ref 后,组件只会重新渲染一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 伪代码:Effect 的 scheduler 用法
function effect(fn, options = {}) {
// ...
effectFn.scheduler = options.scheduler || (() => effectFn()); // 可以自定义调度函数
// ...
effectFn();
return effectFn;
}

// 在 trigger 中使用 scheduler
dep.forEach(effectFn => {
if (effectFn.scheduler) {
effectFn.scheduler(effectFn); // 加入调度队列
} else {
effectFn(); // 直接执行
}
});

// Vue 内部的调度队列 (简化)
const queue = [];
let isFlushing = false;

function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job);
if (!isFlushing) {
isFlushing = true;
Promise.resolve().then(flushJobs); // 微任务队列
}
}
}

function flushJobs() {
// 确保执行顺序 (例如父组件更新在其子组件之前)
queue.sort((a,b) => a.id - b.id);
while(queue.length) {
queue[0]();
queue.shift();
}
isFlushing = false;
}

// 那么,Effect 的 scheduler 就可以是 queueJob
// effect(fn, { scheduler: queueJob });

4.4 ref 的特殊处理

ref 内部存储的是一个原始值或者是被 reactive 转换后的对象。当访问 ref.value 时,它的 get 拦截器会收集依赖;当修改 ref.value 时,它的 set 拦截器会派发更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ref 伪代码
function ref(value) {
const r = {
_value: value,
dep: new Set() // ref 自身的依赖集合
};

return Object.defineProperty(r, 'value', {
enumerable: true,
configurable: true,
get() {
track(r, 'value'); // 收集依赖,注意这里 track 的 target 是 r 对象本身
return r._value;
},
set(newVal) {
if (newVal === r._value) return;
r._value = newVal;
trigger(r, 'value'); // 派发更新
}
});
}

五、Vue 2 与 Vue 3 响应式对比总结

特性/原理 Vue 2 Vue 3
核心机制 Object.defineProperty Proxy
监听范围 只能监听对象已存在的属性,初始化时递归深层遍历 拦截整个对象的操作,按需对嵌套对象进行代理 (惰性)
新增/删除属性 无法直接检测,需 Vue.set/Vue.delete 原生支持检测
数组变动 重写数组方法,无法直接通过索引或修改 length 检测 原生支持检测 (Proxy 可拦截 set/deleteProperty)
性能 初始化性能开销较大 (深层遍历) 初始化性能开销小 (惰性代理) & 运行时性能优化
API Options API (data, computed, watch) Composition API (reactive, ref, computed, watch)
缺陷 有限的响应式能力 几乎没有限制的完全响应式能力
原始值响应式 不支持,需包装在对象中如 { value: 0 } ref() 提供对原始值的响应式支持

六、总结

Vue 3 的响应式系统是其核心竞争力之一。通过从 Object.defineProperty 切换到 Proxy,Vue 3 彻底解决了 Vue 2 中响应性的一些根本性限制,提供了更强大、更高效、更灵活的响应式能力。

  • Proxy 实现了对对象操作的全面拦截,原生支持属性的增删改、数组操作等。
  • reactive() 用于创建响应式对象,而 ref() 用于创建响应式原始值和包装对象。
  • computed()watch() / watchEffect() 构建在响应式系统的之上,提供强大的派生状态和副作用管理能力。
  • 内部的 track (依赖收集) 和 trigger (派发更新) 机制,配合 WeakMap 的数据结构和调度器,确保了高效且批量的 UI 更新。

深入理解这些原理不仅能帮助开发者更好地利用 Vue 3 的强大功能,还能在遇到问题时进行有效的调试和性能优化。掌握 Vue 3 的响应式系统,是成为一名优秀 Vue 开发者的重要一步。