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 引入),定期查看官方文档的变更日志。