Vue 3 响应式基础全面教学:从核心到进阶全面解析
欢迎来到这篇关于 Vue 3 响应式系统的全面教学文档!作为一名新手,你可能会觉得 Vue 的响应式系统(
ref和reactive)有点复杂,但别担心!这篇文档将以通俗易懂的方式,结合详细的解释和实际代码示例,带你一步步掌握 Vue 3 的所有响应式 API。我们会从基础到高级,覆盖ref、reactive及其相关工具函数,确保内容全面、准确,并且适合初学者理解。
我们会按照以下结构来讲解:
- 响应式系统的核心概念:帮你理解 Vue 3 响应式系统的基本原理。
- 核心 API:详细讲解
ref和reactive的用法。 - 工具函数:覆盖
isRef、unref、toRef、toRefs等实用工具。 - 高级 API:深入探讨
shallowRef、triggerRef、customRef等高级功能。 - 注意事项与常见问题:帮助新手避坑。
- 官方文档与参考资料:提供最新资源链接。
每部分都会包含:
- 通俗解释:用生活化的比喻解释概念。
- 代码示例:真实的、可运行的代码片段。
- 注意事项:新手容易犯错的地方和最佳实践。
准备好了吗?让我们开始吧!
一、Vue 3 响应式系统的核心概念
1. 什么是响应式?
想象你有一个笔记本,上面记录了你的每日开销。当你添加一笔新的开销时,笔记本会“自动”更新你的总支出,并且页面上的数字也会实时变化。这就是 Vue 的 响应式:当数据发生变化时,界面会自动更新,而不需要你手动操作 DOM。
Vue 3 的响应式系统基于 Proxy(代理)和 Ref 两种机制:
ref:用于处理基本数据类型(如数字、字符串)或简单对象,包装成一个响应式对象。reactive:用于处理复杂对象(如嵌套的对象或数组),让整个对象变成响应式的。
2. 为什么需要 ref 和 reactive?
在 Vue 2 中,响应式是通过 Object.defineProperty 实现的,但它有局限性,比如无法检测对象属性的添加或删除。Vue 3 引入了 Proxy,让响应式系统更强大,同时提供了 ref 和 reactive 两种方式来满足不同场景的需求:
ref:适合单个值的响应式管理,简单直观。reactive:适合复杂数据结构的响应式管理,比如多层嵌套的对象。
二、核心 API 详解
1. ref:响应式基本值
通俗解释
ref 就像一个魔法盒子,里面装着一个值(可以是数字、字符串、对象等)。你通过 .value 访问或修改盒子里的内容,当内容变化时,Vue 会自动通知界面更新。
用法
- 创建:通过
ref函数创建一个响应式引用。 - 访问/修改:使用
.value获取或设置值。 - 适用场景:适合简单数据,如计数器、输入框的值等。
示例代码
import { ref } from 'vue';
export default {
setup() {
// 创建一个响应式的计数器
const count = ref(0);
// 定义一个增加计数器的方法
const increment = () => {
count.value++; // 修改 .value,触发界面更新
console.log('当前计数:', count.value);
};
return { count, increment };
}
};
<template>
<div>
<p>计数:{{ count }}</p>
<button @click="increment">加 1</button>
</div>
</template>
运行结果
- 页面显示:
计数:0 - 点击按钮后,
count增加,页面自动更新为计数:1、计数:2等。
注意事项
- 必须通过
.value访问:在 JavaScript 代码中,ref是一个对象,值存储在.value属性中。 - 模板中无需
.value:在 Vue 模板中,Vue 会自动解包ref,直接写{{ count }}即可。 - 适合基本类型:
ref常用于数字、字符串等简单数据。如果是对象,建议考虑reactive。
2. reactive:响应式对象
通俗解释
reactive 就像一个智能文件夹,里面可以装很多文件(属性)。当你修改文件夹里的任何文件时,Vue 会自动感知并更新界面。
用法
- 创建:通过
reactive函数将对象变为响应式。 - 访问/修改:直接操作对象的属性,无需
.value。 - 适用场景:适合复杂数据结构,如嵌套对象或数组。
示例代码
import { reactive } from 'vue';
export default {
setup() {
// 创建一个响应式的用户信息对象
const user = reactive({
name: '小明',
age: 20
});
// 修改用户信息的函数
const updateUser = () => {
user.age++; // 直接修改属性,触发界面更新
user.name = '小红';
};
return { user, updateUser };
}
};
<template>
<div>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<button @click="updateUser">更新用户信息</button>
</div>
</template>
运行结果
- 初始显示:
姓名:小明,年龄:20 - 点击按钮后,更新为:
姓名:小红,年龄:21
注意事项
- 只能用于对象:
reactive不支持基本类型(如数字、字符串),这些需要用ref。 - 直接操作属性:不需要
.value,直接用user.name访问或修改。 - 不能重新赋值:不能用
user = { ... }替换整个对象,否则会丢失响应式。需要修改已有属性或使用Object.assign。
三、工具函数详解
Vue 3 提供了一系列工具函数来增强 ref 和 reactive 的使用,下面逐一讲解。
1. isRef:判断是否为 ref
通俗解释
isRef 就像一个鉴定师,告诉你某个变量是不是一个 ref 魔法盒子。
用法
- 作用:检查一个值是否为
ref对象。 - 返回值:
true(是ref)或false(不是ref)。
示例代码
import { ref, reactive, isRef } from 'vue';
export default {
setup() {
const count = ref(0);
const user = reactive({ name: '小明' });
const normalValue = 42;
console.log(isRef(count)); // true
console.log(isRef(user)); // false
console.log(isRef(normalValue)); // false
return { count };
}
};
注意事项
- 用途:常用于调试或需要动态处理不同类型数据时。
- 局限性:只检测
ref,不会检测reactive。
2. unref:获取 ref 的值
通俗解释
unref 就像打开魔法盒子,直接取出里面的值。如果不是 ref,就返回原值。
用法
- 作用:如果传入的是
ref,返回其.value;否则返回原值。 - 适用场景:需要统一处理
ref和非ref值时。
示例代码
import { ref, unref } from 'vue';
export default {
setup() {
const count = ref(10);
const normalValue = 20;
console.log(unref(count)); // 10
console.log(unref(normalValue)); // 20
// 动态处理函数
const getValue = (val) => {
return unref(val); // 自动解包 ref
};
console.log(getValue(count)); // 10
console.log(getValue(normalValue)); // 20
return { count };
}
};
注意事项
- 等价于
toValue:在 Vue 3.3+ 中,unref是toValue的别名,功能完全相同。 - 简化代码:避免手动判断是否为
ref再用.value。
3. toRef:将对象属性转为 ref
通俗解释
toRef 就像从一个文件夹(reactive 对象)里拿出一页纸,单独装进一个魔法盒子(ref),但这页纸仍然与文件夹保持同步。
用法
- 作用:从
reactive对象的属性创建一个ref,保持响应式连接。 - 适用场景:需要单独操作对象的一个属性,但仍希望它与原对象同步。
示例代码
import { reactive, toRef } from 'vue';
export default {
setup() {
const user = reactive({
name: '小明',
age: 20
});
// 将 user.name 转为 ref
const nameRef = toRef(user, 'name');
// 修改 nameRef,user.name 也会同步变化
const updateName = () => {
nameRef.value = '小红';
console.log(user.name); // 小红
};
// 修改 user.name,nameRef 也会同步变化
user.name = '小刚';
console.log(nameRef.value); // 小刚
return { nameRef, updateName };
}
};
<template>
<div>
<p>姓名:{{ nameRef }}</p>
<button @click="updateName">更改姓名</button>
</div>
</template>
注意事项
- 保持同步:
toRef创建的ref与原reactive对象的属性是双向绑定的。 - 必须存在属性:
toRef(user, 'name')要求user中有name属性,否则会报错。 - 性能优化:适合需要单独传递某个属性到子组件或函数时使用。
4. toRefs:将对象所有属性转为 ref
通俗解释
toRefs 就像把整个文件夹(reactive 对象)里的每一页纸都装进单独的魔法盒子(ref),但每个盒子仍然与文件夹保持同步。
用法
- 作用:将
reactive对象的每个属性转为单独的ref。 - 适用场景:需要将
reactive对象的属性解构后仍保持响应式。
示例代码
import { reactive, toRefs } from 'vue';
export default {
setup() {
const user = reactive({
name: '小明',
age: 20
});
// 将 user 的所有属性转为 ref
const { name, age } = toRefs(user);
// 修改 name,user.name 也会变化
name.value = '小红';
console.log(user.name); // 小红
// 修改 user.age,age 也会变化
user.age = 21;
console.log(age.value); // 21
return { name, age };
}
};
<template>
<div>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
</div>
</template>
注意事项
- 解构保持响应式:
toRefs的核心作用是让reactive对象的属性在解构后仍然保持响应式。如果直接解构const { name, age } = user,name和age会变成普通值,失去响应式。 - 批量转换:
toRefs会将对象的所有可枚举属性转为ref,包括动态添加的属性。 - 性能考虑:如果对象属性非常多,
toRefs会为每个属性创建一个ref,可能增加内存开销,谨慎用于大型对象。 - 与
toRef的区别:toRef只针对单个属性,而toRefs针对整个对象,适合需要将所有属性传递给子组件或函数的场景。
5. toValue:统一获取值
通俗解释
toValue 就像一个“万能开箱器”,不管你给它的是 ref、函数、还是普通值,它都能帮你取出最终的值。如果是 ref,它返回 .value;如果是函数,它调用函数并返回结果;如果是普通值,就直接返回。
用法
- 作用:统一处理
ref、函数或普通值,获取其值。 - 适用场景:当你不确定传入的值是
ref、函数还是普通值,但需要一个确定的值时。 - 注意:
toValue是 Vue 3.3+ 引入的新 API,与unref功能类似,但更强大,因为它还能处理函数。
示例代码
import { ref, toValue } from 'vue';
export default {
setup() {
const count = ref(10);
const normalValue = 20;
const getValue = () => 30;
// 使用 toValue 获取值
console.log(toValue(count)); // 10
console.log(toValue(normalValue)); // 20
console.log(toValue(getValue)); // 30
// 统一处理值的函数
const displayValue = (val) => {
console.log('值是:', toValue(val));
};
displayValue(count); // 值是:10
displayValue(normalValue); // 值是:20
displayValue(getValue); // 值是:30
return { count };
}
};
<template>
<div>
<p>计数:{{ count }}</p>
</div>
</template>
注意事项
- 与
unref的区别:unref只处理ref和普通值,而toValue额外支持函数,推荐在 Vue 3.3+ 中优先使用toValue。 - 函数调用:如果传入的是函数,
toValue会执行函数,注意函数内部可能有副作用(如修改状态)。 - 性能优化:在需要统一处理多种类型数据的场景中,
toValue能简化代码逻辑。
四、高级 API 详解
1. shallowRef:浅层响应式引用
通俗解释
shallowRef 就像一个“只看表面”的魔法盒子,只有盒子里的直接值(.value)变化时才会触发更新。如果盒子里装的是对象,对象内部的属性变化不会触发响应式。
用法
- 作用:创建一个浅层响应式
ref,只对.value的直接赋值操作响应。 - 适用场景:适合需要优化性能的场景,比如处理大型对象或只需要监控顶层值的变化。
示例代码
import { shallowRef } from 'vue';
export default {
setup() {
// 创建一个 shallowRef,初始值是一个对象
const state = shallowRef({
name: '小明',
age: 20
});
// 修改对象内部属性(不会触发更新)
const updateInner = () => {
state.value.name = '小红'; // 不会触发界面更新
console.log('内部更新:', state.value);
};
// 替换整个对象(会触发更新)
const updateOuter = () => {
state.value = { name: '小刚', age: 21 }; // 触发界面更新
console.log('整体替换:', state.value);
};
return { state, updateInner, updateOuter };
}
};
<template>
<div>
<p>姓名:{{ state.name }}</p>
<p>年龄:{{ state.age }}</p>
<button @click="updateInner">修改内部</button>
<button @click="updateOuter">替换整体</button>
</div>
</template>
运行结果
- 初始显示:
姓名:小明,年龄:20 - 点击“修改内部”:控制台打印更新,但界面不变化。
- 点击“替换整体”:界面更新为
姓名:小刚,年龄:21。
注意事项
- 仅监控
.value:shallowRef只对.value的直接赋值(如state.value = ...)触发响应式。 - 性能优化:适合处理大数据量对象,避免深层响应式带来的性能开销。
- 局限性:不适合需要监控对象内部属性变化的场景,这种情况应使用
ref或reactive。
2. triggerRef:手动触发更新
通俗解释
triggerRef 就像一个“手动刷新按钮”,当你用 shallowRef 且修改了内部对象属性(默认不触发更新)时,可以用 triggerRef 强制通知 Vue 更新界面。
用法
- 作用:手动触发
shallowRef的响应式更新。 - 适用场景:结合
shallowRef使用,当内部属性变化需要强制更新界面时。
示例代码
import { shallowRef, triggerRef } from 'vue';
export default {
setup() {
const state = shallowRef({
name: '小明',
age: 20
});
// 修改内部属性并手动触发更新
const updateInnerWithTrigger = () => {
state.value.name = '小红'; // 默认不触发更新
triggerRef(state); // 手动触发更新
console.log('触发更新:', state.value);
};
return { state, updateInnerWithTrigger };
}
};
<template>
<div>
<p>姓名:{{ state.name }}</p>
<p>年龄:{{ state.age }}</p>
<button @click="updateInnerWithTrigger">修改并触发</button>
</div>
</template>
运行结果
- 初始显示:
姓名:小明,年龄:20 - 点击按钮:界面更新为
姓名:小红,年龄:20。
注意事项
- 仅用于
shallowRef:triggerRef主要为shallowRef设计,普通ref不需要手动触发。 - 谨慎使用:手动触发可能导致意外的界面更新,需确保逻辑清晰。
- 性能优化:适合在明确知道需要更新时减少不必要的响应式开销。
3. customRef:自定义响应式引用
通俗解释
customRef 就像一个“DIY魔法盒子”,你可以自定义盒子如何存储值、如何响应变化,甚至可以添加延迟、防抖等高级功能。
用法
- 作用:通过工厂函数创建一个自定义的
ref,允许你控制值的获取(get)和设置(set)逻辑。 - 适用场景:需要自定义响应式行为,比如防抖、节流、或与外部数据源同步。
示例代码(防抖输入)
import { customRef } from 'vue';
export default {
setup() {
// 自定义防抖 ref
const debouncedText = customRef((track, trigger) => {
let value = '';
let timeout;
return {
get() {
track(); // 追踪依赖
return value;
},
set(newValue) {
clearTimeout(timeout); // 清除之前的定时器
timeout = setTimeout(() => {
value = newValue; // 更新值
trigger(); // 触发响应式更新
}, 500); // 500ms 防抖
}
};
});
return { debouncedText };
}
};
<template>
<div>
<input v-model="debouncedText" placeholder="输入内容" />
<p>输入内容:{{ debouncedText }}</p>
</div>
</template>
运行结果
- 输入内容后,500ms 内连续输入不会立即更新,只有停止输入 500ms 后,界面才会显示最新值。
注意事项
- 必须调用
track和trigger:track():在get中调用,告诉 Vue 追踪依赖。trigger():在set中调用,通知 Vue 更新界面。
- 灵活但复杂:
customRef功能强大,但逻辑复杂,适合高级场景。 - 性能优化:可以用来实现防抖、节流等优化,减少不必要的更新。
4. shallowReactive:浅层响应式对象
通俗解释
shallowReactive 就像一个“只管第一层”的智能文件夹。文件夹里的直接内容(顶层属性)变化会触发界面更新,但如果文件夹里还有子文件夹(嵌套对象),子文件夹里的内容变化不会触发更新。这种浅层响应式设计是为了优化性能,避免不必要的深层监听。
用法
- 作用:创建一个浅层响应式对象,仅对对象的顶层属性变化触发响应式更新。
- 适用场景:适合处理大型嵌套对象,只需要监控顶层属性的场景,比如配置对象或大数据量的状态管理。
示例代码
import { shallowReactive } from 'vue';
export default {
setup() {
// 创建一个浅层响应式对象
const state = shallowReactive({
user: {
name: '小明',
age: 20
},
count: 0
});
// 修改顶层属性(会触发更新)
const updateTopLevel = () => {
state.count++; // 触发界面更新
console.log('顶层更新:', state.count);
};
// 修改嵌套属性(不会触发更新)
const updateNested = () => {
state.user.name = '小红'; // 不会触发界面更新
console.log('嵌套更新:', state.user.name);
};
return { state, updateTopLevel, updateNested };
}
};
<template>
<div>
<p>姓名:{{ state.user.name }}</p>
<p>计数:{{ state.count }}</p>
<button @click="updateTopLevel">更新计数</button>
<button @click="updateNested">更新姓名</button>
</div>
</template>
运行结果
- 初始显示:
姓名:小明,计数:0 - 点击“更新计数”:界面更新为
计数:1,姓名:小明不变。 - 点击“更新姓名”:控制台打印
小红,但界面不更新,姓名仍显示小明。
注意事项
- 仅监控顶层属性:
shallowReactive只对对象的直接属性(如state.count)变化触发响应式,嵌套对象(如state.user.name)的变化不会触发。 - 性能优化:适合处理大型数据结构,避免深层 Proxy 监听的性能开销。
- 局限性:如果需要嵌套对象的响应式,使用
reactive而不是shallowReactive。 - 不能直接替换对象:与
reactive类似,不能用state = { ... }替换整个对象,否则会丢失响应式。
5. readonly:只读响应式对象
通俗解释
readonly 就像把你的智能文件夹(reactive 或 ref)锁上,只允许查看里面的内容,但不能修改。任何尝试修改的操作都会被阻止,并抛出警告。这种只读特性非常适合保护数据,防止意外修改。
用法
- 作用:将
ref或reactive对象转为只读的响应式对象,禁止修改。 - 适用场景:在组件间传递数据时,确保数据不被子组件或其他代码修改;或者用于状态管理的只读副本。
示例代码
import { reactive, readonly } from 'vue';
export default {
setup() {
// 创建一个响应式对象
const original = reactive({
name: '小明',
age: 20
});
// 创建只读副本
const readOnlyState = readonly(original);
// 尝试修改只读对象
const tryModify = () => {
try {
readOnlyState.name = '小红'; // 会抛出警告,修改无效
} catch (e) {
console.warn('无法修改只读对象!');
}
console.log('只读对象:', readOnlyState.name); // 仍为 小明
};
// 修改原始对象(会触发只读对象的更新)
const updateOriginal = () => {
original.name = '小刚'; // 触发界面更新
console.log('原始对象更新:', readOnlyState.name); // 小刚
};
return { readOnlyState, tryModify, updateOriginal };
}
};
<template>
<div>
<p>姓名:{{ readOnlyState.name }}</p>
<p>年龄:{{ readOnlyState.age }}</p>
<button @click="tryModify">尝试修改</button>
<button @click="updateOriginal">更新原始对象</button>
</div>
</template>
运行结果
- 初始显示:
姓名:小明,年龄:20 - 点击“尝试修改”:控制台抛出警告,界面不变化。
- 点击“更新原始对象”:界面更新为
姓名:小刚,年龄:20。
注意事项
- 只读特性:
readonly创建的对象无法直接修改,尝试修改会抛出警告(开发环境中)。 - 与原始对象同步:
readonly对象是原始对象的代理,原始对象的变化会反映到只读对象上。 - 适用场景:常用于 Vuex/Pinia 的状态只读副本,或者防止子组件修改父组件传递的 props。
- 深层只读:
readonly是深层的,嵌套对象的所有属性也都是只读的。
6. shallowReadonly:浅层只读响应式对象
通俗解释
shallowReadonly 就像只给智能文件夹的第一层加锁,顶层属性不能修改,但嵌套对象(子文件夹)的属性可以自由修改。它是 readonly 的浅层版本,适合需要部分保护的场景。
用法
- 作用:创建一个浅层只读响应式对象,仅顶层属性不可修改,嵌套对象属性可修改。
- 适用场景:需要保护顶层属性,但允许嵌套对象被修改的场景,比如配置对象的部分保护。
示例代码
import { reactive, shallowReadonly } from 'vue';
export default {
setup() {
// 创建一个响应式对象
const original = reactive({
user: {
name: '小明',
age: 20
},
count: 0
});
// 创建浅层只读对象
const shallowReadOnly = shallowReadonly(original);
// 尝试修改
const tryModify = () => {
try {
shallowReadOnly.count = 1; // 抛出警告,修改无效
} catch (e) {
console.warn('无法修改顶层属性!');
}
shallowReadOnly.user.name = '小红'; // 可以修改嵌套属性
console.log('嵌套属性更新:', shallowReadOnly.user.name); // 小红
};
// 修改原始对象
const updateOriginal = () => {
original.count++; // 触发界面更新
console.log('原始对象更新:', shallowReadOnly.count);
};
return { shallowReadOnly, tryModify, updateOriginal };
}
};
<template>
<div>
<p>姓名:{{ shallowReadOnly.user.name }}</p>
<p>计数:{{ shallowReadOnly.count }}</p>
<button @click="tryModify">尝试修改</button>
<button @click="updateOriginal">更新原始对象</button>
</div>
</template>
运行结果
- 初始显示:
姓名:小明,计数:0 - 点击“尝试修改”:嵌套属性更新为
姓名:小红,但count不变,控制台抛出警告。 - 点击“更新原始对象”:界面更新为
计数:1,姓名保持小红。
注意事项
- 仅顶层只读:
shallowReadonly只保护顶层属性,嵌套对象属性可自由修改。 - 性能优化:比
readonly更轻量,适合只需要保护顶层属性的场景。 - 与原始对象同步:修改原始对象的顶层或嵌套属性会反映到
shallowReadonly对象上。 - 适用场景:适合需要部分只读保护的场景,比如只保护配置对象的某些字段。
7. markRaw:标记为非响应式
通俗解释
markRaw 就像给一个对象贴上“禁止响应式”的标签,告诉 Vue 不要将它转为响应式对象。不管是放在 ref、reactive 还是其他响应式对象中,这个对象都不会被 Proxy 包装。
用法
- 作用:标记一个对象为非响应式,防止 Vue 自动将其转为
reactive或ref。 - 适用场景:需要引入第三方库的对象(如 Three.js、Chart.js)或不需要响应式的复杂数据时,优化性能。
示例代码
import { reactive, markRaw } from 'vue';
export default {
setup() {
// 第三方库对象(假设)
const thirdPartyObj = markRaw({
data: '我是第三方数据',
doSomething() {
console.log('执行第三方逻辑');
}
});
// 创建响应式对象
const state = reactive({
name: '小明',
thirdParty: thirdPartyObj // 不会被转为响应式
});
// 修改属性
const update = () => {
state.name = '小红'; // 触发更新
state.thirdParty.data = '新数据'; // 不会触发更新
console.log('第三方对象:', state.thirdParty.data); // 新数据
};
return { state, update };
}
};
<template>
<div>
<p>姓名:{{ state.name }}</p>
<p>第三方数据:{{ state.thirdParty.data }}</p>
<button @click="update">更新</button>
</div>
</template>
运行结果
- 初始显示:
姓名:小明,第三方数据:我是第三方数据 - 点击“更新”:界面更新为
姓名:小红,第三方数据在界面不更新(仍显示我是第三方数据),但控制台打印新数据。
注意事项
- 完全非响应式:
markRaw标记的对象及其所有嵌套属性都不会触发响应式更新。 - 性能优化:适合处理不需要响应式的大型对象或第三方库实例。
- 不可逆:一旦标记为
markRaw,对象无法再被转为响应式。 - 谨慎使用:确保确实不需要响应式,否则可能导致界面更新异常。
8. effectScope:管理响应式副作用
通俗解释
effectScope 就像一个“任务管理器”,可以把多个响应式副作用(比如 watch 或 computed)组织在一起,统一控制它们的生命周期。你可以随时停止整个任务组,避免内存泄漏。
用法
- 作用:创建一个作用域,用于收集和管理响应式副作用(
effect),并提供批量停止的功能。 - 适用场景:动态创建多个
watch或computed,需要统一销毁时(比如动态组件或插件系统)。
示例代码
import { reactive, effectScope, watch } from 'vue';
export default {
setup() {
const scope = effectScope(); // 创建作用域
const state = reactive({ count: 0 });
// 在作用域内定义副作用
scope.run(() => {
watch(
() => state.count,
(newValue) => {
console.log('计数变化:', newValue);
}
);
});
// 增加计数
const increment = () => {
state.count++;
};
// 停止所有副作用
const stopEffects = () => {
scope.stop(); // 停止作用域内所有副作用
console.log('副作用已停止');
};
return { state, increment, stopEffects };
}
};
<template>
<div>
<p>计数:{{ state.count }}</p>
<button @click="increment">加 1</button>
<button @click="stopEffects">停止副作用</button>
</div>
</template>
运行结果
- 初始显示:
计数:0 - 点击“加 1”:计数增加,控制台打印
计数变化:1、计数变化:2等。 - 点击“停止副作用”:控制台打印
副作用已停止,后续计数变化不再触发watch。
注意事项
- 统一管理副作用:
effectScope适合需要动态创建和销毁副作用的场景,比如动态组件或插件。 - 调用
scope.run:副作用必须在scope.run中定义,才能被作用域管理。 - 停止后不可恢复:调用
scope.stop()后,作用域内的所有副作用(如watch、computed)都会停止,且无法重新启用。 - 内存管理:在组件卸载时使用
effectScope确保清理副作用,防止内存泄漏。
9. computed:计算属性(与响应式结合)
通俗解释
computed 就像一个“智能计算器”,它根据响应式数据(ref 或 reactive)自动计算结果,并缓存结果。只有当依赖的数据变化时,它才会重新计算,非常适合需要动态计算的场景。
用法
- 作用:创建一个基于响应式数据的计算属性,只有依赖变化时才会重新计算。
- 适用场景:需要根据响应式数据衍生新数据的场景,比如格式化数据、计算总和等。
示例代码
import { ref, computed } from 'vue';
export default {
setup() {
const price = ref(100);
const quantity = ref(2);
// 创建计算属性:总价
const total = computed(() => {
return price.value * quantity.value;
});
// 修改价格或数量
const update = () => {
price.value += 10;
quantity.value++;
};
return { price, quantity, total, update };
}
};
<template>
<div>
<p>单价:{{ price }}</p>
<p>数量:{{ quantity }}</p>
<p>总价:{{ total }}</p>
<button @click="update">更新</button>
</div>
</template>
运行结果
- 初始显示:
单价:100,数量:2,总价:200 - 点击“更新”:更新为
单价:110,数量:3,总价:330。
注意事项
- 缓存机制:
computed会缓存结果,只有当依赖(如price.value或quantity.value)变化时才重新计算。 - 只读默认:默认的
computed是只读的,尝试修改会抛出警告。 - 可写计算属性:可以通过提供
get和set函数创建可写的计算属性(见下文)。 - 性能优化:比直接在模板中计算更高效,适合复杂的计算逻辑。
可写计算属性示例
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref('小');
const lastName = ref('明');
// 可写计算属性
const fullName = computed({
get() {
return firstName.value + lastName.value;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split('');
}
});
// 修改全名
const updateName = () => {
fullName.value = '小红'; // 触发 setter
};
return { fullName, updateName };
}
};
<template>
<div>
<p>全名:{{ fullName }}</p>
<button @click="updateName">更新全名</button>
</div>
</template>
运行结果
- 初始显示:
全名:小明 - 点击“更新全名”:更新为
全名:小红。
注意事项(可写计算属性)
- 提供
get和set:get返回计算值,set定义如何根据新值更新依赖。 - 谨慎使用:可写计算属性可能增加代码复杂性,确保逻辑清晰。
- 适用场景:适合需要双向绑定的衍生数据,比如表单输入的格式化。
五、注意事项与常见问题
为了帮助新手避坑,以下总结了使用 ref 和 reactive 相关 API 时常见的错误和最佳实践:
1. 常见错误
- 直接解构
reactive对象:- 错误:
const { name } = reactive({ name: '小明' })会导致name失去响应式。 - 解决:使用
toRefs:const { name } = toRefs(reactive({ name: '小明' }))。
- 错误:
- 替换整个
reactive对象:- 错误:
state = { name: '新值' }会破坏响应式。 - 解决:修改属性(如
state.name = '新值')或使用Object.assign(state, { name: '新值' })。
- 错误:
- 在
ref中忘记.value:- 错误:在
setup中直接用count++而不是count.value++。 - 解决:始终在 JavaScript 中使用
.value访问或修改ref值。
- 错误:在
- 误用
shallowRef或shallowReactive:- 错误:期望嵌套对象属性变化触发更新,但使用了
shallowRef或shallowReactive。 - 解决:需要深层响应式时,使用
ref或reactive。
- 错误:期望嵌套对象属性变化触发更新,但使用了
- 忽略
readonly的只读特性:- 错误:尝试修改
readonly或shallowReadonly的顶层属性。 - 解决:确保只修改原始对象,或明确知道
shallowReadonly的嵌套属性可改。
- 错误:尝试修改
2. 最佳实践
- 选择合适的响应式 API:
- 基本类型(数字、字符串)用
ref。 - 复杂对象或数组用
reactive。 - 大型对象且只关心顶层变化时用
shallowRef或shallowReactive。 - 需要保护数据时用
readonly或shallowReadonly。
- 基本类型(数字、字符串)用
- 使用
toRefs解构:- 在需要解构
reactive对象时,始终使用toRefs保持响应式。
- 在需要解构
- 优化性能:
- 使用
shallowRef、shallowReactive或markRaw处理大数据量或第三方库对象。 - 使用
effectScope管理动态副作用,防止内存泄漏。
- 使用
- 调试响应式问题:
- 使用
isRef、isReactive、isReadonly等工具函数检查变量类型。 - 启用 Vue 的开发模式,查看控制台的警告信息。
- 使用
- 清晰的命名:
- 为
ref和reactive变量取有意义的名字,比如countRef、userState,避免混淆。
- 为
- 结合状态管理:
- 在大型应用中,结合 Pinia 或 Vuex 使用
reactive和readonly管理全局状态。
- 在大型应用中,结合 Pinia 或 Vuex 使用
六、总结与选择指南
Vue 3 的响应式系统提供了灵活且强大的工具,涵盖了从简单值到复杂对象的各种场景。以下是快速选择指南,帮助你决定何时使用哪个 API:
| API | 适用场景 | 注意事项 |
|---|---|---|
ref |
基本类型或简单对象 | 使用 .value 访问/修改;在模板中自动解包 |
reactive |
复杂对象或数组 | 不能替换整个对象;直接操作属性 |
isRef |
检查是否为 ref |
用于调试或动态处理数据 |
unref/toValue |
统一获取 ref 或普通值 |
toValue 支持函数,Vue 3.3+ 推荐使用 |
toRef |
将 reactive 属性转为 ref |
保持与原对象同步;属性必须存在 |
toRefs |
解构 reactive 对象保持响应式 |
适合批量传递属性到子组件 |
shallowRef |
浅层响应式,优化大数据量 | 仅监控 .value 变化,嵌套属性不响应 |
triggerRef |
手动触发 shallowRef 更新 |
配合 shallowRef 使用,避免滥用 |
customRef |
自定义响应式逻辑(如防抖、节流) | 需手动调用 track 和 trigger,逻辑复杂 |
shallowReactive |
浅层响应式对象,优化性能 | 仅监控顶层属性,嵌套属性不响应 |
readonly |
保护数据不被修改 | 深层只读,修改抛出警告 |
shallowReadonly |
保护顶层属性,允许嵌套修改 | 适合部分保护的场景 |
markRaw |
标记对象为非响应式 | 用于第三方库对象或不需要响应式的数据,优化性能 |
effectScope |
统一管理响应式副作用 | 动态组件或插件中管理 watch、computed,需调用 scope.stop() 清理 |
computed |
动态计算衍生数据 | 缓存结果,默认只读,可提供 get/set 实现可写 |
七、官方文档与参考资料
以下是 Vue 3 响应式系统的最新官方文档链接(基于 Vue 3.5.x,2025 年 5 月)以及推荐的社区资源,帮助你深入学习和查阅:
1. 官方文档
- 核心响应式 API:ref, reactive, computed 等
- 响应式工具函数:isRef, unref, toRef, toRefs, toValue 等
- 高级响应式 API:shallowRef, triggerRef, customRef, shallowReactive, readonly, shallowReadonly, markRaw, effectScope 等
- Vue 3 指南:响应式基础
- Vue 3 API 总览:全局 API
2. 社区资源
- Vue Mastery:提供 Vue 3 响应式系统的视频教程,适合初学者。
- Vue.js Developers Blog:Vue 3 的最佳实践和案例。
- Stack Overflow:搜索 Vue 3 响应式相关问题,获取社区解答。
3. 学习建议
- 实践为主:通过小项目练习
ref和reactive,比如实现一个计数器、表单或 Todo 列表。 - 阅读源码:Vue 3 的响应式系统代码在 GitHub 的
vuejs/core仓库,阅读@vue/reactivity部分有助于深入理解。 - 调试工具:使用 Vue Devtools 浏览器插件,观察响应式数据的变化。
- 关注更新:Vue 3 持续更新(如
toValue在 3.3 引入),定期查看官方文档的变更日志。