Vue3响应式系统深入解析
前言
Vue 3 的响应式系统是其核心特性之一,相比 Vue 2 有了显著的改进和性能优化。本文将深入解析 Vue 3 响应式系统的实现原理、核心 API 以及使用技巧,帮助开发者更好地理解和运用 Vue 3 的响应式能力。
一、响应式系统演进
1. Vue 2 与 Vue 3 响应式系统对比
Vue 2 的响应式系统基于 Object.defineProperty 实现,而 Vue 3 则采用了 ES6 的 Proxy。这种变化带来了多方面的改进:
| 特性 |
Vue 2 (Object.defineProperty) |
Vue 3 (Proxy) |
| 属性添加/删除 |
不支持直接监听,需要 Vue.set/Vue.delete |
原生支持监听属性添加和删除 |
| 数组索引和长度 |
对数组索引和长度的变化支持有限 |
完全支持数组索引和长度的变化 |
| 嵌套对象 |
需要递归遍历所有层级属性 |
懒代理,仅在访问时才代理嵌套对象 |
| 性能 |
初始化较慢,内存占用较大 |
初始化更快,内存占用更小 |
| 类型支持 |
对 TypeScript 支持有限 |
更好的 TypeScript 集成支持 |
2. Proxy 的优势
ES6 Proxy 提供了拦截对象操作的能力,Vue 3 利用这一特性实现了更强大、更灵活的响应式系统:
- 完整的对象操作拦截:可以拦截 get、set、deleteProperty 等更多操作
- 懒代理:只在实际访问属性时才递归代理子属性
- 更好的数组支持:可以直接监听数组索引、长度变化和数组方法
- 无需手动维护依赖追踪:依赖收集和触发更新更加自动化和精确
二、核心响应式 API 详解
1. reactive
reactive 函数用于创建一个响应式对象或数组:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { reactive } from 'vue';
// 创建响应式对象
const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 25
},
tags: ['Vue', 'JavaScript', 'TypeScript']
});
// 自动触发更新
state.count++; // 触发更新
state.user.name = 'Bob'; // 触发更新
state.tags.push('React'); // 触发更新
|
reactive 的工作原理是返回一个原始对象的 Proxy 代理,通过陷阱函数拦截对对象的操作:
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
|
// reactive 简化实现原理
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key);
// 获取原始值
const result = Reflect.get(target, key, receiver);
// 懒代理:如果是对象,递归代理
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
// 设置新值
const result = Reflect.set(target, key, value, receiver);
// 触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const hasKey = key in target;
const result = Reflect.deleteProperty(target, key);
// 如果属性存在并成功删除,触发更新
if (hasKey) {
trigger(target, key, 'delete');
}
return result;
}
});
}
|
2. ref
ref 函数用于创建一个包装基本类型值的响应式引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { ref } from 'vue';
// 创建响应式引用
const count = ref(0);
const message = ref('Hello Vue 3');
const isLoading = ref(false);
// 通过 .value 访问和修改
console.log(count.value); // 0
count.value++; // 触发更新
message.value = 'Hello Reactive'; // 触发更新
// 在模板中自动解包
// <template>
// <div>{{ count }}</div> <!-- 不需要 .value -->
// </template>
|
ref 的工作原理是创建一个包含 .value 属性的包装对象,然后对 .value 属性进行响应式处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// ref 简化实现原理
function ref(value) {
const refObject = {
get value() {
// 依赖收集
track(refObject, 'value');
return value;
},
set value(newValue) {
if (value !== newValue) {
value = newValue;
// 触发更新
trigger(refObject, 'value');
}
}
};
// 添加 __v_isRef 标识
Object.defineProperty(refObject, '__v_isRef', {
value: true
});
return refObject;
}
|
3. computed
computed 函数用于创建一个计算属性,它会根据响应式依赖自动计算并缓存结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { ref, computed } from 'vue';
const count = ref(0);
// 创建计算属性
const doubleCount = computed(() => count.value * 2);
const isEven = computed(() => count.value % 2 === 0);
// 计算属性会根据依赖自动更新
count.value = 1; // doubleCount 自动变为 2,isEven 自动变为 false
// 计算属性默认是只读的
// doubleCount.value = 10; // 错误:计算属性是只读的
// 创建可写计算属性
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (newValue) => {
const [first, last] = newValue.split(' ');
firstName.value = first;
lastName.value = last;
}
});
|
computed 的工作原理是维护一个缓存值和脏标志,只有当依赖变化时才重新计算:
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
|
// computed 简化实现原理
function computed(getterOrOptions) {
let getter;
let setter;
// 处理传入的是函数还是对象
if (typeof getterOrOptions === 'function') {
getter = getterOrOptions;
setter = () => console.warn('Computed property is readonly');
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
let dirty = true;
let value;
let computed;
// 创建计算对象
const runner = effect(getter, {
lazy: true,
scheduler: () => {
// 依赖变化时标记为脏
dirty = true;
// 触发依赖此计算属性的更新
trigger(computed, 'value');
}
});
computed = {
get value() {
// 依赖收集
track(computed, 'value');
// 只有脏时才重新计算
if (dirty) {
value = runner();
dirty = false;
}
return value;
},
set value(newValue) {
setter(newValue);
}
};
return computed;
}
|
4. watch
watch 函数用于监听响应式数据的变化并执行回调:
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
|
import { ref, reactive, watch } from 'vue';
// 监听单个 ref
const count = ref(0);
watch(count, (newCount, oldCount) => {
console.log(`count changed from ${oldCount} to ${newCount}`);
});
// 监听 reactive 对象的属性
const state = reactive({ count: 0, name: 'Vue' });
watch(
() => state.count,
(newCount, oldCount) => {
console.log(`count changed from ${oldCount} to ${newCount}`);
}
);
// 监听多个源
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}`);
console.log(`name: ${oldName} -> ${newName}`);
});
// 深度监听
watch(
() => state,
(newState, oldState) => {
console.log('State changed:', newState);
},
{ deep: true }
);
// 立即执行
watch(count, (newCount) => {
console.log('Count:', newCount);
}, { immediate: true });
|
5. watchEffect
watchEffect 函数用于创建一个副作用函数,并自动追踪其依赖的响应式数据:
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
|
import { ref, watchEffect } from 'vue';
const count = ref(0);
const name = ref('Vue');
// 创建副作用,自动追踪所有依赖
const stop = watchEffect(() => {
console.log(`Count: ${count.value}, Name: ${name.value}`);
});
// 当不再需要监听时停止副作用
// stop();
// 清除副作用
watchEffect((onCleanup) => {
// 副作用执行
const timer = setTimeout(() => {
console.log('Timer executed');
}, 1000);
// 清除函数会在下一次副作用执行前或组件卸载时调用
onCleanup(() => {
clearTimeout(timer);
});
});
|
三、响应式系统的实现原理
1. 依赖收集与触发
Vue 3 响应式系统的核心是依赖收集和触发更新机制:
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
// 依赖收集和触发的核心实现
// 当前活跃的副作用函数
let activeEffect;
// 副作用栈,处理嵌套副作用
const effectStack = [];
// 副作用函数类
class ReactiveEffect {
constructor(fn, scheduler = null) {
this.fn = fn;
this.scheduler = scheduler;
this.deps = [];
this.active = true;
}
run() {
// 非激活状态只执行函数,不收集依赖
if (!this.active) {
return this.fn();
}
// 确保副作用函数正确入栈和出栈
if (!effectStack.includes(this)) {
try {
// 入栈
effectStack.push(this);
// 设置当前活跃副作用
activeEffect = this;
// 执行副作用函数,触发依赖收集
return this.fn();
} finally {
// 出栈
effectStack.pop();
// 恢复之前的活跃副作用
activeEffect = effectStack[effectStack.length - 1];
}
}
}
stop() {
if (this.active) {
// 清理所有依赖
cleanupEffect(this);
this.active = false;
}
}
}
// 创建副作用
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
// 立即执行一次
_effect.run();
// 返回停止函数
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
// 依赖收集
function track(target, key) {
if (!activeEffect) return;
// 获取 target 的依赖映射
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 获取 key 对应的副作用集合
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 将当前活跃副作用添加到集合
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
// 同时记录依赖到副作用的 deps 中
activeEffect.deps.push(dep);
}
}
// 触发更新
function trigger(target, key, type) {
// 获取依赖映射
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 获取要执行的副作用集合
const effects = new Set();
// 添加 key 对应的副作用
if (key !== undefined) {
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => effects.add(effect));
}
}
// 执行副作用
effects.forEach(effect => {
// 如果有调度器,则使用调度器
if (effect.scheduler) {
effect.scheduler();
} else {
// 否则直接运行副作用
effect.run();
}
});
}
|
2. 响应式系统的内部结构
Vue 3 响应式系统的内部结构可以概括为以下几个部分:
- 原始数据:用户定义的数据对象或值
- 代理对象:通过 Proxy 创建的响应式代理
- 依赖映射:跟踪数据与副作用函数之间的关系
- 副作用函数:响应数据变化而执行的函数
- 调度器:控制副作用函数的执行时机和方式
1
|
原始数据 -> Proxy 代理 -> 拦截操作 -> 依赖收集/触发更新 -> 副作用函数执行
|
3. 响应式数据的类型
Vue 3 支持多种类型的响应式数据:
- reactive 对象:通过
reactive() 创建的响应式对象
- ref 对象:通过
ref() 创建的包含 .value 的响应式引用
- computed 对象:通过
computed() 创建的计算属性
- readonly 对象:通过
readonly() 创建的只读响应式对象
四、高级使用技巧
1. 响应式数据的解构问题
解构赋值会丢失响应性,需要使用 toRefs 或 toRef 来保留响应性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { reactive, toRefs, toRef } from 'vue';
const state = reactive({ count: 0, name: 'Vue' });
// 错误:解构后丢失响应性
const { count, name } = state;
count = 1; // 不会触发更新
// 正确:使用 toRefs 保留响应性
const { count: countRef, name: nameRef } = toRefs(state);
countRef.value = 1; // 会触发更新
// 只转换单个属性
const countRef = toRef(state, 'count');
|
2. 响应式数据的类型转换
Vue 3 提供了多个工具函数用于响应式数据的类型转换:
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
|
import { reactive, ref, toRefs, toRef, isRef, isReactive, isProxy } from 'vue';
const reactiveObj = reactive({ count: 0 });
const countRef = ref(0);
const plainObj = { count: 0 };
// 检查是否为 ref
console.log(isRef(countRef)); // true
console.log(isRef(reactiveObj)); // false
// 检查是否为 reactive
console.log(isReactive(reactiveObj)); // true
console.log(isReactive(countRef)); // false
// 检查是否为代理对象(reactive 或 readonly)
console.log(isProxy(reactiveObj)); // true
console.log(isProxy(countRef)); // false
// 将 ref 转换为原始值
const count = unref(countRef); // 等同于 countRef.value
// 如果是 ref 则返回其 value,否则返回原始值
function useFeature(id) {
const unwrappedId = unref(id); // id 可以是 ref 或普通值
// ...
}
|
3. 响应式数据的深拷贝
处理响应式数据的深拷贝需要特殊处理,避免丢失响应性:
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
|
import { reactive, toRaw } from 'vue';
const state = reactive({
user: { name: 'Alice', age: 25 },
tags: ['Vue', 'React']
});
// 获取原始数据,用于序列化等操作
const rawState = toRaw(state);
const json = JSON.stringify(rawState);
// 深拷贝响应式数据
function deepCloneReactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
// 处理数组
if (Array.isArray(target)) {
const result = reactive([]);
for (let i = 0; i < target.length; i++) {
result[i] = deepCloneReactive(target[i]);
}
return result;
}
// 处理对象
const result = reactive({});
for (const key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
result[key] = deepCloneReactive(target[key]);
}
}
return result;
}
|
4. 自定义响应式行为
可以通过 customRef 创建自定义的响应式引用:
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
|
import { customRef } from 'vue';
// 创建带有防抖功能的 ref
function useDebouncedRef(value, delay = 300) {
let timeout;
return customRef((track, trigger) => ({
get() {
// 依赖收集
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
// 触发更新
trigger();
}, delay);
}
}));
}
// 使用自定义 ref
const searchQuery = useDebouncedRef('');
// 创建可缓存的 ref
function useCachedRef(key, initialValue) {
// 尝试从 localStorage 获取缓存值
const cachedValue = localStorage.getItem(key);
let value = cachedValue !== null ? JSON.parse(cachedValue) : initialValue;
return customRef((track, trigger) => ({
get() {
track();
return value;
},
set(newValue) {
value = newValue;
// 缓存到 localStorage
localStorage.setItem(key, JSON.stringify(newValue));
trigger();
}
}));
}
|
五、常见问题与解决方案
1. 响应式数据更新但视图不渲染
问题分析:
- 可能是因为修改了未被代理的属性
- 可能是因为在非响应式上下文中修改了数据
- 可能是因为修改了数组的非响应式方法
解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { reactive, ref, nextTick } from 'vue';
// 问题:直接修改数组长度不触发更新
const arr = reactive([1, 2, 3]);
arr.length = 0; // 不会触发更新
// 解决方案:使用响应式方法
arr.splice(0); // 会触发更新
// 问题:直接添加不存在的属性
const obj = reactive({ name: 'Vue' });
obj.age = 3; // Vue 3 已经支持这种方式
// 问题:在定时器中修改数据
const count = ref(0);
setTimeout(() => {
count.value++; // 应该会触发更新,但如果不触发...
// 可以使用 nextTick 确保 DOM 更新
nextTick(() => {
// DOM 已更新
});
}, 1000);
|
2. 循环引用导致的问题
问题分析:
循环引用可能导致无限递归和栈溢出,特别是在序列化或深拷贝时。
解决方案:
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
|
import { reactive } from 'vue';
// 创建循环引用的响应式对象
const parent = reactive({ name: 'Parent' });
const child = reactive({ name: 'Child' });
// 创建循环引用
parent.child = child;
child.parent = parent;
// 序列化时的解决方案
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
}
return value;
});
}
console.log(safeStringify(parent));
|
3. 性能优化技巧
避免不必要的响应式转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import { reactive, markRaw } from 'vue';
// 对于不需要响应式的大型对象,使用 markRaw 跳过响应式转换
const state = reactive({
// 普通响应式数据
count: 0,
// 第三方库实例、DOM 对象等不需要响应式的数据
chartInstance: markRaw(new Chart()),
// 大型静态配置对象
config: markRaw({
// 大量配置项...
})
});
|
合理使用 computed 缓存:
1
2
3
4
5
6
7
8
9
10
11
|
import { computed } from 'vue';
// 对于复杂计算,使用 computed 缓存结果
const expensiveResult = computed(() => {
// 复杂计算逻辑
let result = 0;
for (let i = 0; i < largeArray.length; i++) {
result += complexCalculation(largeArray[i]);
}
return result;
});
|
避免在模板中进行复杂计算:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<template>
<!-- 不推荐:在模板中进行复杂计算 -->
<div>{{ items.filter(item => item.active).length }}</div>
<!-- 推荐:使用计算属性 -->
<div>{{ activeItemsCount }}</div>
</template>
<script setup>
import { computed } from 'vue';
// 使用计算属性缓存结果
const activeItemsCount = computed(() => {
return items.filter(item => item.active).length;
});
</script>
|
六、响应式系统与组合式 API 结合
1. 构建可复用的响应式逻辑
使用组合式 API 封装可复用的响应式逻辑:
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
|
// src/composables/useCounter.js
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
// 响应式状态
const count = ref(initialValue);
// 计算属性
const isEven = computed(() => count.value % 2 === 0);
const isPositive = computed(() => count.value > 0);
// 修改状态的方法
function increment(step = 1) {
count.value += step;
}
function decrement(step = 1) {
count.value -= step;
}
function reset() {
count.value = initialValue;
}
// 返回需要暴露的状态和方法
return {
count,
isEven,
isPositive,
increment,
decrement,
reset
};
}
// 使用示例
import { useCounter } from '@/composables/useCounter';
const { count, increment, reset } = useCounter(10);
increment(); // count.value 变为 11
reset(); // count.value 重置为 10
|
2. 构建响应式的 API 调用
封装响应式的 API 调用逻辑:
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
// src/composables/useApi.js
import { ref, reactive } from 'vue';
export function useApi() {
const loading = ref(false);
const error = ref(null);
async function request(url, options = {}) {
loading.value = true;
error.value = null;
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (err) {
error.value = err.message;
throw err;
} finally {
loading.value = false;
}
}
// GET 请求
function get(url, params = {}) {
const queryString = new URLSearchParams(params).toString();
const fullUrl = queryString ? `${url}?${queryString}` : url;
return request(fullUrl, {
method: 'GET'
});
}
// POST 请求
function post(url, data = {}) {
return request(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
// PUT 请求
function put(url, data = {}) {
return request(url, {
method: 'PUT',
body: JSON.stringify(data)
});
}
// DELETE 请求
function del(url) {
return request(url, {
method: 'DELETE'
});
}
return {
loading,
error,
get,
post,
put,
delete: del
};
}
// 使用示例
import { useApi } from '@/composables/useApi';
const { loading, error, get, post } = useApi();
async function fetchUsers() {
try {
const users = await get('/api/users');
// 处理用户数据
} catch (err) {
// 错误处理
}
}
|
总结
Vue 3 的响应式系统是其最强大的特性之一,通过使用 Proxy 实现,相比 Vue 2 提供了更完整、更高效的响应式能力。本文深入解析了 Vue 3 响应式系统的核心 API、实现原理和高级使用技巧,包括:
- 响应式系统演进:从 Vue 2 的 Object.defineProperty 到 Vue 3 的 Proxy 的转变
- 核心响应式 API:reactive、ref、computed、watch 和 watchEffect 的使用和原理
- 实现原理:依赖收集和触发更新的内部机制
- 高级使用技巧:响应式数据的解构、类型转换、深拷贝和自定义响应式行为
- 常见问题与解决方案:响应式数据更新但视图不渲染、循环引用问题和性能优化
- 与组合式 API 结合:构建可复用的响应式逻辑和响应式 API 调用
掌握 Vue 3 响应式系统的原理和使用技巧,对于构建高性能、可维护的 Vue 应用至关重要。通过合理使用响应式 API 和遵循最佳实践,开发者可以充分发挥 Vue 3 的优势,创建出优秀的前端应用。