Vue3 响应式基础

发布于:2025-09-01 ⋅ 阅读:(21) ⋅ 点赞:(0)

响应式是 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:toReftoRefsreadonly 等,用于特定场景

使用响应式数据的注意事项,避免常见陷阱