响应式系统
Vue3响应式原理与API详解
前置知识
- Vue3 性能优化实践:建议先完成前一篇的学习
学习目标
- 掌握「1. 响应式系统概述 | Reactive System Overview」的核心机制、典型用法与常见陷阱
- 掌握「2. 响应式 API | Reactive APIs」的核心机制、典型用法与常见陷阱
- 掌握「3. 响应式工具 | Reactive Utilities」的核心机制、典型用法与常见陷阱
- 掌握「4. 响应式系统的陷阱 | Reactive System Pitfalls」的核心机制、典型用法与常见陷阱
- 掌握「5. 响应式系统的最佳实践 | Reactive System Best Practices」的核心机制、典型用法与常见陷阱
1. 响应式系统概述 | Reactive System Overview
Vue3 的响应式系统是其核心特性之一,它使得数据变化能够自动触发视图更新。与 Vue2 相比,Vue3 的响应式系统进行了重构,使用 ES6 Proxy 替代了 Object.defineProperty,提供了更强大的响应式能力。
1.1 响应式系统的工作原理
Vue3 的响应式系统主要包括以下几个部分:
- 响应式数据:使用
ref或reactive创建的可观察数据 - 依赖追踪:自动追踪组件渲染过程中使用的响应式数据
- 依赖收集:收集组件对响应式数据的依赖
- 触发更新:当响应式数据变化时,自动触发依赖该数据的组件更新
1.2 Vue3 响应式系统的优势
- 更强大的响应式能力:支持更多数据类型,包括 Map、Set 等
- 更好的性能:使用 Proxy 减少了不必要的依赖追踪
- 更简洁的 API:提供了
ref、reactive、computed等简洁的 API - 更好的 TypeScript 支持:类型推断更加准确
2. 响应式 API | Reactive APIs
2.1 ref
ref 用于创建响应式的基本类型数据:
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
ref 也可以用于创建响应式的对象:
import { ref } from 'vue';
const user = ref({
name: 'John',
age: 30
});
console.log(user.value.name) // John
user.value.age = 31
console.log(user.value.age) // 31
ref 可以包装任意类型的值:基本类型在 .value 上直接读写,对象类型则由 .value 内部再套一层 reactive 代理,因此深层属性同样具备响应性。
2.2 reactive
reactive 用于创建响应式的对象:
import { reactive } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
});
console.log(state.count) // 0
state.count++
console.log(state.count) // 1
reactive只接受对象类型(对象、数组、Map、Set),基本类型请使用ref。
2.3 computed
computed 用于创建计算属性,它会根据依赖的响应式数据自动重新计算:
import { ref, computed } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
console.log(doubleCount.value); // 0
count.value++;
console.log(doubleCount.value); // 2
2.4 watch
watch 用于监听数据变化:
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
count.value++; // 输出: Count changed from 0 to 1
watch 也可以监听多个数据源:
import { ref, watch } from 'vue';
const count = ref(0);
const message = ref('Hello');
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
console.log(`Count changed from ${oldCount} to ${newCount}`);
console.log(`Message changed from ${oldMessage} to ${newMessage}`);
});
count.value++; // 输出: Count changed from 0 to 1
message.value = 'Hi'; // 输出: Message changed from Hello to Hi
2.5 watchEffect
watchEffect 用于自动追踪响应式依赖,当依赖变化时重新执行:
import { ref, watchEffect } from 'vue';
const count = ref(0);
const stop = watchEffect(() => {
console.log(`Count is ${count.value}`);
});
count.value++; // 输出: Count is 1
// 停止监听
stop();
count.value++; // 不会输出
3. 响应式工具 | Reactive Utilities
3.1 toRefs
toRefs 用于将响应式对象转换为普通对象,其中每个属性都是一个 ref:
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
});
const refs = toRefs(state)
console.log(refs.count.value) // 0
console.log(refs.message.value) // Hello
// refs.count 是一个 ref,修改它会影响原对象
refs.count.value++
console.log(state.count) // 1
3.2 toRef
toRef 用于为响应式对象的单个属性创建 ref:
import { reactive, toRef } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
});
const countRef = toRef(state, 'count')
console.log(countRef.value) // 0
// 修改 ref 会影响原对象
countRef.value++
console.log(state.count) // 1
3.3 unref
unref 用于获取 ref 的值,如果参数不是 ref,则直接返回参数:
import { ref, unref } from 'vue';
const count = ref(0);
const message = 'Hello';
console.log(unref(count)); // 0
console.log(unref(message)); // Hello
3.4 isRef
isRef 用于检查一个值是否是 ref:
import { ref, isRef } from 'vue';
const count = ref(0);
const message = 'Hello';
console.log(isRef(count)); // true
console.log(isRef(message)); // false
3.5 shallowRef
shallowRef 用于创建浅响应式的 ref,只响应 .value 的变化,不响应内部属性的变化:
import { shallowRef } from 'vue'
const user = shallowRef({
name: 'John',
age: 30
});
// 修改内部属性不会触发更新
user.value.age = 31
// 修改 .value 会触发更新
user.value = {
name: 'John',
age: 31
}
3.6 shallowReactive
shallowReactive 用于创建浅响应式的对象,只响应顶层属性的变化,不响应嵌套属性的变化:
import { shallowReactive } from 'vue'
const state = shallowReactive({
user: {
name: 'John',
age: 30
}
});
// 修改嵌套属性不会触发更新
state.user.age = 31
// 修改顶层属性会触发更新
state.user = {
name: 'John',
age: 31
}
3.7 triggerRef
triggerRef 用于手动触发 shallowRef 的更新:
import { shallowRef, triggerRef } from 'vue'
const user = shallowRef({
name: 'John',
age: 30
});
// 修改内部属性
user.value.age = 31
// 手动触发更新
triggerRef(user)
3.8 customRef
customRef 用于创建自定义的 ref:
import { customRef } from 'vue';
function useDebouncedRef(value, delay = 300) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
},
};
});
}
// 使用自定义 ref
const searchQuery = useDebouncedRef('');
4. 响应式系统的陷阱 | Reactive System Pitfalls
4.1 响应式数据的解构
当你解构响应式对象时,解构出来的值会失去响应性:
import { reactive } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
// 解构会失去响应性
const { count, message } = state
console.log(count) // 0
// 修改原对象
state.count++
console.log(count) // 0 (不会更新)
解决方法是使用 toRefs:
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
// 使用 toRefs 解构
const { count, message } = toRefs(state)
console.log(count.value) // 0
// 修改原对象
state.count++
console.log(count.value) // 1 (会更新)
4.2 响应式对象的引用替换
reactive 返回的是原对象的代理,把变量重新指向一个新对象后,变量持有的只是普通对象,视图绑定的仍然是旧代理,更新因此”丢失”:
import { reactive } from 'vue'
let state = reactive({
count: 0,
message: 'Hello'
})
// 错误:重新赋值后 state 不再是响应式代理
state = {
count: 1,
message: 'Hi'
}
解决方法是修改原对象的属性,而不是整体替换变量引用:
import { reactive } from 'vue'
let state = reactive({
count: 0,
message: 'Hello'
});
// 修改对象的属性,代理不变,响应式保持
state.count = 1
state.message = 'Hi'
如果确实需要”整体换数据”,把数据包在
ref里替换.value,或逐字段赋值。数组同理:用list.splice(0, list.length, ...newItems)而不是list = [...]。
4.3 响应式数据的添加
向响应式对象添加新属性时,新属性会自动成为响应式的,这与 Vue 2 中必须使用 Vue.set 的行为不同:
import { reactive } from 'vue'
const state = reactive({
count: 0
})
// 添加新属性
state.message = 'Hello' // 新属性是响应式的
在 Vue3 中,使用 reactive 创建的对象,添加新属性时会自动成为响应式的,这是因为 Vue3 使用了 Proxy。
4.4 响应式数据的删除
在 Vue 3 中,从响应式对象删除属性会触发更新:Proxy 的 deleteProperty 陷阱会拦截 delete state.message 并通知依赖。Vue.delete 是 Vue 2 时代的 API,在 Vue 3 中已被移除;Reflect.deleteProperty 是 JavaScript 的反射 API,Proxy 内部同样会经过它,业务代码直接使用 delete 操作符即可,不需要手动调用。
import { reactive, watchEffect } from 'vue'
const state = reactive({
count: 0,
message: 'Hello',
})
// 依赖 message 的 effect
watchEffect(() => console.log(state.message))
delete state.message // 触发依赖更新,effect 重新执行
需要留意的边界:shallowReactive 与 markRaw 创建的对象不会深度代理,内部嵌套属性的修改或删除不会触发更新;此时应整体替换引用,或使用 triggerRef 等手动触发手段。
5. 响应式系统的最佳实践 | Reactive System Best Practices
5.1 选择合适的响应式 API
- 基本类型:使用
ref - 对象:使用
reactive - 需要解构的对象:使用
reactive+toRefs - 性能敏感的场景:使用
shallowRef或shallowReactive
5.2 避免过度响应
- 不需要响应式的数据:不要使用响应式 API
- 频繁变化的数据:考虑使用
shallowRef或customRef - 大型对象:考虑使用
shallowReactive
5.3 合理使用计算属性
- 复杂的计算逻辑:使用
computed - 依赖多个响应式数据:使用
computed - 需要缓存计算结果:使用
computed
5.4 合理使用监听器
- 需要执行副作用:使用
watch或watchEffect - 需要监听特定数据:使用
watch - 需要自动追踪依赖:使用
watchEffect - 需要清理副作用:使用
watch或watchEffect的清理函数
6. 示例 | Examples
6.1 响应式数据示例
<template>
<div class="reactive-example">
<h2>Reactive Data Example</h2>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<p>Message: {{ message }}</p>
<button @click="increment">Increment</button>
<button @click="changeMessage">Change Message</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const message = ref('Hello');
const doubleCount = computed(() => count.value * 2);
const increment = () => count.value++;
const changeMessage = () => (message.value = 'Hi');
</script>
<style scoped>
.reactive-example {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
max-width: 400px;
margin: 0 auto;
}
button {
margin: 0 5px;
padding: 5px 10px;
font-size: 16px;
}
</style>
6.2 监听器示例
<template>
<div class="watch-example">
<h2>Watch Example</h2>
<div>
<p>Count: {{ count }}</p>
<p>Message: {{ message }}</p>
<button @click="increment">Increment</button>
<button @click="changeMessage">Change Message</button>
<div>
<h3>Watch Log:</h3>
<ul>
<li v-for="(log, index) in logs" :key="index">{{ log }}</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
const message = ref('Hello');
const logs = ref([]);
// 使用 watch 监听单个数据
watch(count, (newValue, oldValue) => {
logs.value.push(`Count changed from ${oldValue} to ${newValue}`);
});
// 使用 watch 监听多个数据
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
if (newCount !== oldCount) {
logs.value.push(`Count changed from ${oldCount} to ${newCount}`);
}
if (newMessage !== oldMessage) {
logs.value.push(`Message changed from ${oldMessage} to ${newMessage}`);
}
});
// 使用 watchEffect 自动追踪依赖
watchEffect(() => {
logs.value.push(`Current count: ${count.value}, current message: ${message.value}`);
});
const increment = () => count.value++;
const changeMessage = () => (message.value = `Hi ${Math.random()}`);
</script>
<style scoped>
.watch-example {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
max-width: 400px;
margin: 0 auto;
}
button {
margin: 0 5px;
padding: 5px 10px;
font-size: 16px;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 5px 0;
border-bottom: 1px solid #eee;
}
</style>
6.3 计算属性示例
<template>
<div class="computed-example">
<h2>Computed Example</h2>
<div>
<p>First Name: <input v-model="firstName" /></p>
<p>Last Name: <input v-model="lastName" /></p>
<p>Full Name: {{ fullName }}</p>
<p>Full Name Length: {{ fullNameLength }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// 计算全名
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// 计算全名长度
const fullNameLength = computed(() => fullName.value.length);
</script>
<style scoped>
.computed-example {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
max-width: 400px;
margin: 0 auto;
}
input {
width: 200px;
padding: 5px;
}
</style>
7. 小结 | Summary
Vue3 的响应式系统是其核心特性之一,它使用 ES6 Proxy 提供了更强大的响应式能力。通过本章节的学习,你已经了解了 Vue3 响应式系统的基本概念和使用方法,包括响应式 API、响应式工具、响应式系统的陷阱和最佳实践。 响应式系统的核心优势在于它使得数据变化能够自动触发视图更新,减少了手动操作 DOM 的需要,提高了开发效率。在实际开发中,要根据具体场景选择合适的响应式 API,避免过度响应,合理使用计算属性和监听器,以提高应用的性能和可维护性。
基础响应式
ref 响应式引用
const <state> = ref(<initialValue>);
import { ref } from 'vue';
const count = ref(0);
const user = ref({ name: 'Tom' });
count.value++;
user.value.name = 'Jerry';
reactive 对象响应式
const <state> = reactive(<object>);
import { reactive } from 'vue';
const state = reactive({
count: 0,
list: [],
user: { name: 'Tom' }
});
state.count++;
state.list.push('item');
浅层响应式
shallowRef 浅响应式引用
const <state> = shallowRef(<initialValue>);
import { shallowRef } from 'vue';
const obj = shallowRef({ count: 0 });
obj.value.count++; // 不触发
obj.value = { count: 1 }; // 触发:整体替换
shallowReactive 浅响应式对象
const <state> = shallowReactive(<object>);
import { shallowReactive } from 'vue';
const state = shallowReactive({
nested: { count: 0 }
});
state.nested.count = 1; // 不触发,只追踪顶层属性
shallowReadonly 浅只读
const <state> = shallowReadonly(<object>);
import { shallowReadonly } from 'vue';
const state = shallowReadonly({
nested: { count: 0 }
});
state.nested.count = 1; // 允许(只读不递归)
state.foo = 'bar'; // 警告
只读与转换
readonly 深只读
const <readonly> = readonly(<source>);
import { reactive, readonly } from 'vue';
const original = reactive({ count: 0, nested: { value: 1 } });
const frozen = readonly(original);
frozen.count = 1; // 警告
frozen.nested.value = 2; // 警告(深只读)
markRaw 永久标记非响应
const <obj> = markRaw(<object>);
import { reactive, markRaw } from 'vue';
const state = reactive({});
state.classInstance = markRaw(new MyClass());
state.thirdPartyObj = markRaw(largeObject);
toRaw 获取原始对象
const <raw> = toRaw(<proxy>);
import { reactive, toRaw } from 'vue';
const proxy = reactive({ count: 0 });
const raw = toRaw(proxy);
console.log(raw === proxy); // false
Ref 转换
toRef 转换 reactive 属性为 ref
const <ref> = toRef(<source>, <key>);
import { reactive, toRef } from 'vue';
const state = reactive({ count: 0 });
const countRef = toRef(state, 'count');
countRef.value++; // state.count 同步变化
toRef 从 getter 创建
const <ref> = toRef(() => <expression>);
import { toRef } from 'vue';
const state = reactive({ user: { name: 'Tom' } });
const nameRef = toRef(() => state.user.name);
toRefs 解构响应式对象
const { <key>, ... } = toRefs(<reactive>);
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'Tom' });
const { count, name } = toRefs(state);
count.value++;
unref 取值
const <value> = unref(<maybeRef>);
import { ref, unref } from 'vue';
const count = ref(0);
unref(count); // 0
unref(123); // 123
unref(undefined); // undefined
类型守卫
isRef 判断 ref
import { ref, isRef } from 'vue';
isRef(ref(0)); // true
isRef(0); // false
isRef(reactive({})); // false
isReactive 判断 reactive
import { reactive, isReactive } from 'vue';
isReactive(reactive({})); // true
isReactive(ref({})); // false
isReactive({}); // false
isReadonly 判断只读
import { readonly, isReadonly } from 'vue';
isReadonly(readonly({})); // true
isProxy 判断代理
import { reactive, readonly, isProxy } from 'vue';
isProxy(reactive({})); // true
isProxy(readonly({})); // true
isProxy({}); // false
高级响应式
customRef 自定义 ref
const <state> = customRef(<track>, <trigger>);
import { customRef } from 'vue';
function debouncedRef(value, delay = 200) {
let timer;
return customRef((track, trigger) => ({
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
}));
}
const text = debouncedRef('hello', 500);
triggerRef 手动触发 shallowRef
triggerRef(<shallowRef>);
import { shallowRef, triggerRef } from 'vue';
const obj = shallowRef({ count: 0 });
obj.value.count = 1;
triggerRef(obj); // 强制触发依赖
effectScope 副作用作用域
const <scope> = effectScope();
import { effectScope, watchEffect } from 'vue';
const scope = effectScope();
scope.run(() => {
watchEffect(() => console.log('effect 1'));
watchEffect(() => console.log('effect 2'));
});
scope.stop(); // 停止内部所有 effect
getCurrentScope 获取当前作用域
import { getCurrentScope } from 'vue';
const scope = getCurrentScope();
if (scope) {
scope.run(() => { /* ... */ });
}
onScopeDispose 作用域销毁时回调
import { onScopeDispose } from 'vue';
onScopeDispose(() => {
console.log('scope disposed');
cleanup();
});
响应式工具组合
响应式工具综合示例
import { ref, reactive, computed, toRefs, watch } from 'vue';
function useCounter(initial = 0) {
const state = reactive({
count: initial,
double: computed(() => state.count * 2)
});
function increment() {
state.count++;
}
watch(() => state.count, (newVal) => {
console.log('count changed:', newVal);
});
return { ...toRefs(state), increment };
}
响应式数组操作
import { reactive } from 'vue';
const list = reactive<number[]>([]);
list.push(1, 2, 3); // 触发更新
list.splice(0, 1); // 触发更新
list[0] = 99; // Vue 3 中可触发
list.length = 0; // 触发更新
响应式 Map/Set
import { reactive } from 'vue';
const map = reactive(new Map<string, number>());
map.set('a', 1); // 触发更新
map.delete('a'); // 触发更新
const set = reactive(new Set<number>());
set.add(1); // 触发更新
set.has(1); // true