Vue3 响应式基础
响应式是 Vue 的核心特性之一,它允许应用的数据与 DOM 建立自动关联,当数据发生变化时,相关的 DOM 会自动更新。Vue3 对响应式系统进行了全面重构,采用全新的实现方式,提供了更好的性能和更灵活的 API。
一、响应式的基本概念
响应式系统的核心思想是:
当数据被读取时,自动记录依赖(即哪些代码在使用这个数据)
当数据被修改时,自动通知所有依赖该数据的代码重新执行
在 Vue 组件中,模板渲染、计算属性、侦听器等都会自动参与这个响应式过程,开发者无需手动操作 DOM,只需关注数据变化即可。
二、Vue3 响应式系统的实现原理
1. 与 Vue2 的区别
Vue2 使用 Object.defineProperty
实现响应式,而 Vue3 则基于 ES6 的 Proxy
API,带来了以下优势:
特性 | Vue2 (Object.defineProperty) | Vue3 (Proxy) |
---|---|---|
对象新增属性 | 不支持,需使用 Vue.set | 原生支持,无需额外操作 |
对象删除属性 | 不支持,需使用 Vue.delete | 原生支持,无需额外操作 |
数组索引修改 | 不支持 | 原生支持 |
数组 length 修改 | 不支持 | 原生支持 |
集合类型 (Map/Set) | 不支持 | 支持 |
性能 | 初始化时递归遍历所有属性 | 懒代理,访问时才建立响应式 |
2. Proxy 工作原理简介
Proxy
可以创建一个对象的代理,从而实现对目标对象的读取、修改等操作的拦截和自定义处理。Vue3 正是利用这一特性来追踪数据的访问和修改:
// 简单示例:模拟 Vue3 响应式原理 const target = { count: 0 }// 创建代理对象 const proxy = new Proxy(target, {// 拦截读取操作get(target, key) {console.log(`读取了属性 ${key}`)// 记录依赖(依赖收集)track(target, key)return target[key]},// 拦截设置操作set(target, key, value) {console.log(`修改了属性 ${key} 为 ${value}`)target[key] = value// 触发更新(通知依赖)trigger(target, key)return true} })// 使用代理对象 proxy.count // 读取操作,会被 get 拦截 proxy.count = 1 // 修改操作,会被 set 拦截
在实际实现中,Vue3 还处理了嵌套对象、数组、集合类型等复杂情况,并提供了高效的依赖追踪机制。
三、响应式 API:ref 和 reactive
Vue3 提供了两种主要方式创建响应式数据:ref
和 reactive
,适用于不同场景。
1. ref:处理基本类型和引用类型
ref
用于创建可以持有任何类型值的响应式引用,主要特点:
可以处理基本类型(字符串、数字、布尔值等)和引用类型(对象、数组等)
通过 .value
属性访问和修改其值(在模板中使用时无需 .value
)
当值为对象或数组时,会自动通过 reactive
转为响应式代理
基本用法示例:
<template><div><p>计数器:{{ count }}</p><p>用户名:{{ user.name }}</p><button @click="increment">增加</button><button @click="changeName">修改名称</button></div> </template><script setup> import { ref } from 'vue'// 创建基本类型的响应式数据 const count = ref(0)// 创建引用类型的响应式数据 const user = ref({name: '张三',age: 25 })// 修改基本类型值 function increment() {count.value++ // 注意:需要使用 .value }// 修改引用类型值 function changeName() {user.value.name = '李四' // 对象属性修改// 也可以替换整个对象// user.value = { name: '李四', age: 26 } } </script>
2. reactive:处理对象类型
reactive
用于创建对象类型的响应式代理,主要特点:
仅适用于对象类型(对象、数组、Map、Set 等),不能用于基本类型
返回一个响应式代理对象,直接访问和修改属性即可(无需 .value
)
深层响应式:对象的嵌套属性也会自动转为响应式
基本用法示例:
<template><div><p>用户信息:{{ user.name }},{{ user.age }}岁</p><p>技能:{{ skills.join(', ') }}</p><button @click="growUp">增加年龄</button><button @click="addSkill">添加技能</button></div> </template><script setup> import { reactive } from 'vue'// 创建响应式对象 const user = reactive({name: '张三',age: 25,address: {city: '北京' // 嵌套对象也会是响应式的} })// 创建响应式数组 const skills = reactive(['JavaScript', 'Vue'])// 修改对象属性 function growUp() {user.age++ // 直接修改属性,无需 .value// 修改嵌套属性user.address.city = '上海' }// 操作响应式数组 function addSkill() {skills.push('CSS') // 数组方法会触发更新 } </script>
3. ref 与 reactive 的选择
何时使用 ref
,何时使用 reactive
:
基本类型(字符串、数字、布尔值等):必须使用 ref
对象类型:如果需要保持原始对象的引用方式(直接访问属性),使用 reactive
如果需要对整个对象重新赋值,使用 ref
(因为 reactive
返回的代理对象不能直接替换)
推荐实践:优先使用 ref
,它的使用更加一致,尤其是在组合式 API 中
示例:reactive
不能直接替换对象,而 ref
可以:
// reactive 不能直接替换对象 const user = reactive({ name: '张三' }) user = { name: '李四' } // 错误:会失去响应式// ref 可以替换整个对象 const user = ref({ name: '张三' }) user.value = { name: '李四' } // 正确:仍然保持响应式
四、响应式的扩展 API
1. isRef:检查是否为 ref 对象
isRef
用于判断一个值是否为 ref
创建的响应式对象:
import { ref, isRef } from 'vue'const count = ref(0) const name = '张三'console.log(isRef(count)) // true console.log(isRef(name)) // false
2. unref:获取 ref 对象的值
unref
是一个便捷函数,如果参数是 ref
对象则返回其 .value
,否则返回参数本身:
import { ref, unref } from 'vue'const count = ref(0) const name = '张三'console.log(unref(count)) // 0(等价于 count.value) console.log(unref(name)) // '张三'(直接返回)
它相当于 val = isRef(val) ? val.value : val
的简写。
3. toRef:将对象属性转为 ref
toRef
可以将响应式对象的某个属性转为一个 ref
,且保持与原对象的响应式关联:
import { reactive, toRef } from 'vue'const user = reactive({name: '张三',age: 25 })// 将 user.name 转为 ref const nameRef = toRef(user, 'name')// 修改 ref 会影响原对象 nameRef.value = '李四' console.log(user.name) // '李四'// 修改原对象会影响 ref user.name = '王五' console.log(nameRef.value) // '王五'
4. toRefs:将响应式对象转为 ref 对象集合
toRefs
可以将一个响应式对象的所有属性转为 ref
对象,并包装到一个普通对象中:
import { reactive, toRefs } from 'vue'const user = reactive({name: '张三',age: 25 })// 将 user 的所有属性转为 ref const userRefs = toRefs(user)// 每个属性都是 ref 对象 console.log(userRefs.name.value) // '张三' console.log(userRefs.age.value) // 25// 保持响应式关联 userRefs.name.value = '李四' console.log(user.name) // '李四'
常用于在组合式 API 中将响应式对象的属性解构出来,同时保持响应式:
function useUser() {const user = reactive({name: '张三',age: 25})// ... 其他逻辑return toRefs(user) // 返回 ref 集合 }// 在组件中使用 const { name, age } = useUser() // 可以直接使用 name 和 age(ref 对象) console.log(name.value, age.value)
5. isReactive:检查是否为响应式对象
isReactive
用于判断一个对象是否是由 reactive
创建的响应式代理:
import { reactive, isReactive } from 'vue'const user = reactive({ name: '张三' }) const plainObj = { name: '李四' }console.log(isReactive(user)) // true console.log(isReactive(plainObj)) // false
6. shallowRef 与 shallowReactive:浅响应式
默认情况下,ref
和 reactive
都是深层响应式的(嵌套属性也会是响应式的)。Vue3 提供了浅响应式 API:
shallowRef
:只对 .value
的变更做出响应,不处理内部属性的响应式
shallowReactive
:只对对象的顶层属性做出响应,不处理嵌套属性的响应式
适用于性能优化场景,当你明确知道不需要深层响应式时使用:
import { shallowRef, shallowReactive } from 'vue'// 浅 ref const shallowCount = shallowRef({ value: 0 }) // 修改 .value 会触发更新 shallowCount.value = { value: 1 } // 触发更新 // 修改内部属性不会触发更新 shallowCount.value.value = 2 // 不会触发更新// 浅 reactive const shallowUser = shallowReactive({name: '张三',address: { city: '北京' } }) // 修改顶层属性会触发更新 shallowUser.name = '李四' // 触发更新 // 修改嵌套属性不会触发更新 shallowUser.address.city = '上海' // 不会触发更新
7. readonly 与 shallowReadonly:只读代理
创建一个只读的响应式代理,防止对数据的修改:
readonly
:深层只读,所有嵌套属性都不能修改
shallowReadonly
:浅层只读,只有顶层属性不能修改,嵌套属性可以修改
import { reactive, readonly, shallowReadonly } from 'vue'const user = reactive({name: '张三',address: { city: '北京' } })// 深层只读 const readOnlyUser = readonly(user) readOnlyUser.name = '李四' // 警告:不能修改 readOnlyUser.address.city = '上海' // 警告:不能修改// 浅层只读 const shallowReadOnlyUser = shallowReadonly(user) shallowReadOnlyUser.name = '李四' // 警告:不能修改 shallowReadOnlyUser.address.city = '上海' // 允许修改(嵌套属性可改)
常用于保护原始数据不被意外修改,如 props 传递的数据。
五、响应式数据的注意事项
1. 避免直接替换响应式对象
对于 reactive
创建的响应式对象,直接替换整个对象会导致失去响应式:
let user = reactive({ name: '张三' }) user = { name: '李四' } // 错误:新对象不是响应式的
解决方案:
使用 ref
包装对象,通过 .value
替换
修改对象的属性而非替换整个对象
2. 数组操作注意事项
虽然 Vue3 支持通过索引修改数组,但在某些情况下,推荐使用数组方法以获得更好的性能:
const list = reactive([1, 2, 3])// 支持但不推荐 list[0] = 100// 推荐方式 list.splice(0, 1, 100) // 或使用 ref const list = ref([1, 2, 3]) list.value[0] = 100 // 支持且常用
3. 响应式转换是“深层”的
ref
(对象类型)和 reactive
会递归地将所有嵌套属性转为响应式,这在大多数情况下很方便,但对于大型数据结构可能影响性能。此时可以考虑使用 shallowRef
或 shallowReactive
。
4. 解构响应式对象会失去响应式
直接解构 reactive
创建的响应式对象会导致属性失去响应式:
const user = reactive({ name: '张三', age: 25 }) const { name, age } = user // 非响应式name = '李四' // 不会更新 UI console.log(user.name) // 仍然是 '张三'
解决方案:使用 toRefs
将属性转为 ref
后再解构:
const user = reactive({ name: '张三', age: 25 }) const { name, age } = toRefs(user) // 都是 ref 对象name.value = '李四' // 会更新 UI console.log(user.name) // '李四'
六、总结
Vue3 的响应式系统是其核心功能之一,基于 Proxy
实现,提供了比 Vue2 更强大、更灵活的响应式能力。主要知识点包括:
响应式的基本概念:依赖收集与触发更新
Vue3 响应式基于 Proxy
,相比 Vue2 有诸多优势
核心 API:ref
(处理所有类型)和 reactive
(处理对象类型)
扩展 API:toRef
、toRefs
、readonly
等,用于特定场景
使用响应式数据的注意事项,避免常见陷阱