在迁移 Vue2 项目到 Vue3 时,最让我困惑的不是 Composition API 的语法变化,而是响应式系统的底层逻辑重构。当发现ref(1)和reactive({count:1})在模板中使用时的差异,以及setup函数中访问方式的不同后,我意识到必须深入理解响应式系统的工作原理才能用好这两个 API。本文将从源码实现、组件应用和实战技巧三个维度,用程序员的视角解析 Vue3 的响应式数据机制。

响应式数据的底层实现机制

Vue3 的响应式系统基于 ES6 的 Proxy 实现,与 Vue2 的 Object.defineProperty 相比,能更好地支持数组变化监测和动态属性添加。ref和reactive作为对外暴露的 API,虽然使用方式不同,但最终都通过 Proxy 实现数据劫持。

先看reactive的核心实现逻辑(简化版源码):


// reactive核心实现

function reactive(target) {

// 只处理对象类型(数组也是对象)

if (typeof target !== 'object' || target === null) {

return target

}

// 创建Proxy代理

return new Proxy(target, {

// 拦截属性读取

get(target, key, receiver) {

// 收集依赖

track(target, key)

// 递归处理嵌套对象

const result = Reflect.get(target, key, receiver)

if (typeof result === 'object' && result !== null) {

return reactive(result)

}

return result

},

// 拦截属性设置

set(target, key, value, receiver) {

const oldValue = Reflect.get(target, key, receiver)

// 如果值未变化则不触发更新

if (oldValue === value) return true

// 设置属性值

const result = Reflect.set(target, key, value, receiver)

// 触发更新

trigger(target, key)

return result

},

// 拦截属性删除

deleteProperty(target, key) {

const hasKey = Reflect.has(target, key)

const result = Reflect.deleteProperty(target, key)

if (hasKey && result) {

// 触发更新

trigger(target, key)

}

return result

}

})

}

这段代码展示了reactive的核心逻辑:通过 Proxy 创建对象的代理,在get拦截中收集依赖,在set和deleteProperty拦截中触发更新。值得注意的是对嵌套对象的递归处理 —— 当访问对象的属性仍是对象时,会自动将其转为响应式对象,这就是为什么reactive能深度监测对象变化。

ref的实现则更复杂一些,因为它需要处理基本类型(string、number、boolean 等)的响应式:


// ref核心实现

function ref(value) {

// 创建ref对象

const refObject = {

// 标记为ref类型

__v_isRef: true,

// 存储原始值

_value: convert(value),

// getter:访问.value时触发

get value() {

// 收集依赖

track(refObject, 'value')

return refObject._value

},

// setter:设置.value时触发

set value(newValue) {

if (newValue === refObject._value) return

refObject._value = convert(newValue)

// 触发更新

trigger(refObject, 'value')

}

}

return refObject

}

// 转换值:如果是对象则转为reactive

function convert(value) {

return typeof value === 'object' && value !== null ? reactive(value) : value

}

ref通过包裹对象的方式实现基本类型的响应式 —— 将值存储在_value属性中,通过value的 getter/setter 实现依赖收集和更新触发。当传入的是对象类型时,会自动调用reactive进行处理,这就是为什么ref({})和reactive({})在底层实现上是相通的。

在组件中使用时,这两种 API 的主要区别体现在访问方式上:


// ref与reactive的访问差异

const countRef = ref(0)

const userReactive = reactive({ name: '张三' })

console.log(countRef.value) // 必须通过.value访问

console.log(userReactive.name) // 直接访问属性

// 当ref存储对象时

const userRef = ref({ age: 18 })

console.log(userRef.value.age) // 需要两层访问

这种差异在模板中会被 Vue 自动处理 —— 模板中使用ref时不需要手动添加.value,Vue 的编译阶段会自动展开,但在setup函数、computed或方法中必须显式使用.value。

组件中的响应式数据应用

在 Composition API 中,ref和reactive是构建组件状态的基础。根据数据类型和使用场景选择合适的 API,能让代码更简洁、性能更优。

处理基本类型数据时,ref是最佳选择。以下是一个计数器组件的实现:


<template>

<div class="counter">

<p>当前计数:{{ count }}</p>

<button @click="increment">+1</button>

<button @click="decrement">-1</button>

</div>

</template>

<script setup>

import { ref } from 'vue'

// 基本类型响应式数据

const count = ref(0)

// 方法定义

const increment = () => {

count.value++ // 必须使用.value

}

const decrement = () => {

count.value--

}

</script>

对于对象类型数据,reactive能提供更自然的访问方式。以下是用户信息表单组件:


<template>

<form class="user-form">

<input

type="text"

v-model="user.name"

placeholder="姓名"

>

<input

type="number"

v-model="user.age"

placeholder="年龄"

>

<p>用户信息:{{ user }}</p>

</form>

</template>

<script setup>

import { reactive } from 'vue'

// 对象类型响应式数据

const user = reactive({

name: '',

age: 0

})

// 注意:不能直接替换整个对象

// 错误示例:这会丢失响应性

// user = { name: '李四', age: 20 }

// 正确做法:更新属性

const resetUser = () => {

user.name = ''

user.age = 0

}

</script>

实际开发中,经常需要将响应式对象解构为普通变量使用,但直接解构会丢失响应性。这时可以使用toRefs将reactive对象转换为ref对象的集合:


<script setup>

import { reactive, toRefs } from 'vue'

const user = reactive({

name: '张三',

age: 18

})

// 将reactive对象转换为ref集合

const { name, age } = toRefs(user)

// 解构后仍保持响应性

const changeName = () => {

name.value = '李四' // 仍需使用.value

}

</script>

toRefs的作用是为对象的每个属性创建对应的ref,这样解构后的数据仍然保持响应性连接。这在将组件状态拆分到多个组合函数时特别有用:


// 组合函数:处理用户信息

function useUser() {

const user = reactive({

name: '',

age: 0

})

// 方法

const setName = (newName) => {

user.name = newName

}

// 返回ref集合,方便组件解构使用

return {

...toRefs(user),

setName

}

}

// 在组件中使用

<script setup>

import { useUser } from './composables/useUser'

const { name, age, setName } = useUser()

</script>

这种模式能让组合函数返回的状态在组件中被灵活使用,同时保持响应性。

响应式数据的实战技巧与注意事项

在实际项目中,错误使用ref和reactive可能导致响应性丢失或性能问题。掌握这些 API 的边界情况和优化技巧,能让代码更健壮。

处理数组时,reactive的表现有时会出人意料:


// 数组响应式处理

const books = reactive([

{ id: 1, title: 'Vue实战' },

{ id: 2, title: 'JavaScript高级程序设计' }

])

// 正确:直接修改数组元素属性

books[0].title = 'Vue3实战' // 响应式有效

// 正确:使用数组方法

books.push({ id: 3, title: 'React设计模式' }) // 响应式有效

// 危险:直接替换数组

books = [/* 新数组 */] // 响应性丢失

// 正确:清空数组后添加新元素

books.length = 0

newBooks.forEach(book => books.push(book))

// 正确:使用索引替换元素

books.splice(0, 1, { id: 1, title: 'Vue3实战' })

当需要替换整个数组时,更好的做法是使用ref包裹数组:


// 更安全的数组处理方式

const books = ref([

{ id: 1, title: 'Vue实战' }

])

// 直接替换整个数组(响应性保持)

books.value = [

{ id: 1, title: 'Vue3实战' },

{ id: 2, title: 'React设计模式' }

]

ref包裹的数组在替换时只需更新.value,比reactive的数组处理更直观,这也是我在项目中处理数组时优先选择ref的原因。

另一个容易出错的场景是解构ref数组:


// 错误示例:解构后丢失响应性

const bookList = ref([{ id: 1 }, { id: 2 }])

const [book1, book2] = bookList.value // 普通对象,无响应性

// 正确做法:保持数组的ref引用

const getBook = (index) => {

return bookList.value[index] // 仍能触发响应式

}

// 或者使用toRefs处理数组

const bookRefs = toRefs(bookList.value)

console.log(bookRefs[0].value.id) // 响应式有效

在性能优化方面,对于大型对象,应避免使用reactive进行深层响应式转换。可以使用shallowRef或shallowReactive创建浅层响应式数据:


// 浅层响应式:只监测顶层属性

const shallowUser = shallowReactive({

name: '张三',

address: { city: '北京' } // 深层对象无响应性

})

// 修改顶层属性:有效

shallowUser.name = '李四'

// 修改深层属性:无响应性

shallowUser.address.city = '上海' // 不会触发更新

// 正确做法:替换整个深层对象

shallowUser.address = { city: '上海' } // 有效

shallowRef和shallowReactive适用于不需要深层响应式的场景,如第三方库实例、大型数据集合等,能显著提升性能。

在处理 DOM 元素时,ref还有一个特殊用途 —— 获取元素引用:


<template>

<div ref="container"></div>

<input ref="usernameInput" type="text">

</template>

<script setup>

import { ref, onMounted } from 'vue'

// 创建DOM引用

const container = ref(null)

const usernameInput = ref(null)

onMounted(() => {

// DOM挂载后才能访问

console.log(container.value.clientWidth) // 容器宽度

usernameInput.value.focus() // 自动聚焦

})

</script>

这种用法利用了ref的容器特性,Vue 会在组件挂载时自动将 DOM 元素赋值给ref的.value属性。

总结一下ref和reactive的选择原则:

  1. 基本类型(string、number、boolean)或单值对象(如日期)使用ref
  1. 复杂对象类型使用reactive
  1. 需要解构的对象使用reactive配合toRefs
  1. 数组类型优先使用ref
  1. 大型对象或不需要深层响应式的场景使用浅层 API

理解 Vue3 响应式系统的工作原理后,会发现ref和reactive的设计非常合理 —— 它们分别解决了不同数据类型的响应性问题,同时保持了 API 的简洁性。在实际项目中,我通常遵循 "基本类型用 ref,对象用 reactive,解构用 toRefs" 的原则,配合浅层响应式 API 进行性能优化,既能保证响应性正常工作,又能避免不必要的性能开销。

随着对这些 API 理解的深入,你会发现 Vue3 的响应式系统比 Vue2 更加灵活和强大,特别是在处理复杂状态管理时,这种优势会更加明显。作为开发者,花时间掌握这些底层机制,远比死记 API 用法更有价值。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐