本文是 Vue3 系列第三篇,将深入探讨 Vue3 的响应式系统。响应式是 Vue 框架的核心魔法,它让数据变化能够自动触发界面更新,就像给数据注入了生命力一样。理解响应式原理,能够帮助我们编写出更高效、更可靠的 Vue 应用。
一、什么是响应式?
想象一下,你在 Excel 表格中设置了一个公式 =A1+B1,当 A1 或 B1 单元格的值发生变化时,显示公式结果的单元格会自动更新。Vue 的响应式系统就是基于类似的原理,但功能要强大得多。
在 Vue 中,响应式意味着当数据发生变化时,所有依赖这个数据的地方都会自动更新。这包括模板中的显示、计算属性、侦听器等。这种自动更新的机制让我们从繁琐的 DOM 操作中解放出来,能够更专注于业务逻辑。
简单来说,响应式就是:"数据变,界面自动变"的魔法。当你修改了数据,所有使用这个数据的地方都会像多米诺骨牌一样连锁反应,自动更新显示最新的值。这种机制让我们的开发工作变得异常简单,不再需要手动操作 DOM 来更新界面。
二、ref:基本类型的响应式
ref 是 Vue3 中用于创建响应式数据的基本函数,它特别适合处理基本数据类型(string、number、boolean 等)。可以把 ref 想象成一个智能的包装盒,它把普通的值包装起来,让这个值具备被 Vue 追踪变化的能力。
ref 的基本使用
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 创建响应式数据
const count = ref(0)
const increment = () => {
count.value++ // 注意这里需要 .value
console.log('当前计数:', count.value)
}
</script>
代码解释:
这段代码展示了 ref 的基本用法。我们首先导入 ref 函数,然后用它创建了一个响应式的计数器 count,初始值为 0。在模板中,我们直接使用 {``{ count }} 来显示这个值,当点击按钮时,调用 increment 函数,通过 count.value++ 来修改这个值。
关键点说明:
-
ref(0)创建了一个响应式引用,包装了数字 0 -
在 JavaScript 中访问时需要
.value,因为count实际上是一个包装对象 -
在模板中不需要
.value,Vue 会自动解包 -
每次修改
count.value都会触发界面重新渲染
控制台中的 ref 对象
让我们看看在控制台中 ref 创建的数据是什么样子:
TypeScript
// 普通变量
const a = '1'
console.log(a) // 输出: "1" - 这是一个普通的字符串
// ref 创建的响应式变量
const b = ref('1')
console.log(b) // 输出: RefImpl { _value: "1", __v_isRef: true, ... }
详细解释:
当你打印 ref('1') 时,会看到一个 RefImpl 对象(Ref Implement 的缩写,意思是引用实现)。这个对象有几个重要特点:
-
_value属性存储了实际的值 "1" -
__v_isRef标记这是一个 ref 对象 -
所有以下划线开头的属性都是 Vue 内部使用的,我们不应该直接操作它们
这个包装机制让 Vue 能够追踪到数据的变化。当 b.value 被修改时,Vue 能知道这个变化,然后自动更新所有使用到 b 的地方。
为什么需要 .value?
这是一个很重要的概念。ref 将基本数据类型包装成一个对象,这样 Vue 就能够追踪到这个数据的变化。在 JavaScript 中,基本类型(string、number 等)是按值传递的,如果直接使用,Vue 无法知道它们什么时候被修改。
通过包装成对象,Vue 就可以通过对象的引用来追踪变化。这就是为什么我们需要通过 .value 来访问实际的值。
但是在模板中,Vue 会自动帮我们解包 ,所以我们不需要写 .value:
TypeScript
// 在 JavaScript 中需要 .value
count.value = 10
console.log(count.value) // 10
// 在模板中不需要 .value
// <p>{{ count }}</p> 正确 - Vue 自动解包
// <p>{{ count.value }}</p> 错误 - 会显示 [object Object]
这种设计是经过深思熟虑的:在模板中保持简洁易读,在 JavaScript 中明确表明我们在操作响应式数据。
三、reactive:创建对象类型的响应式
对于对象和数组这样的复杂数据类型,Vue 提供了 reactive 函数来创建响应式数据。如果说 ref 是给基本类型值穿上了"响应式外衣",那么 reactive 就是给整个对象注入了"响应式灵魂"。
reactive 的基本使用
html
<template>
<div>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="updateUser">更新用户</button>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
// 普通对象
const normalUser = { name: '张三', age: 18 }
// 响应式对象
const user = reactive({ name: '张三', age: 18 })
const updateUser = () => {
user.name = '李四'
user.age = 25
console.log('用户信息已更新')
}
</script>
代码解释:
这段代码展示了 reactive 的用法。我们创建了两个对象:normalUser 是普通对象,user 是响应式对象。当点击按钮时,我们修改 user 的属性,这些修改会自动反映到界面上。
关键点说明:
-
reactive直接接收一个对象并返回响应式代理 -
访问属性时不需要
.value,直接user.name即可 -
修改任何属性都会触发界面更新
-
适合处理复杂的对象数据结构
控制台中的 reactive 对象
让我们在控制台查看这两种对象的区别:
TypeScript
console.log(normalUser) // 输出: {name: "张三", age: 18}
console.log(user) // 输出: Proxy {name: "张三", age: 18}
详细解释:
你会发现 reactive 创建的对象被包装成了 Proxy 对象。Proxy 是 ES6 的强大特性,它允许 Vue 拦截对对象的所有操作(读取、赋值、删除属性等)。
当你在代码中执行 user.name = '李四' 时,实际上发生了这些事:
-
Vue 的 Proxy 拦截器捕获到这个赋值操作
-
更新实际的值
-
通知所有依赖
user.name的地方进行更新 -
触发界面重新渲染
这种机制让 Vue 能够精确地知道数据发生了什么变化,以及需要更新哪些部分。
reactive 的深度响应式
reactive 的一个强大特性是深度响应式,这意味着嵌套的对象也会自动变成响应式的:
TypeScript
const deepData = reactive({
user: {
profile: {
name: '张三',
hobbies: ['读书', '编程'] // 数组也是响应式的
}
}
})
// 所有这些修改都会触发更新
deepData.user.profile.name = '李四' // 修改嵌套属性
deepData.user.profile.hobbies.push('游泳') // 修改数组
深度响应式的意义:
这意味着你不需要为每个嵌套对象手动创建响应式,reactive 会自动处理整个对象树 。无论数据有多深,只要是通过 reactive 创建的,所有层次的修改都能被追踪到。
在实际开发中,当你看到控制台打印出 Proxy,就知道这个对象已经是响应式的了。这是判断一个对象是否为响应式的简单方法。
四、ref 也能处理对象类型
你可能会好奇:既然有专门的 reactive 处理对象,为什么还要用 ref 来处理对象呢?这确实是个好问题。实际上,ref 在设计上就很灵活,它能够处理所有类型的数据。
ref 处理对象类型
html
<template>
<div>
<p>姓名: {{ user.name }}</p>
<button @click="updateUser">更新</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 使用 ref 创建对象类型的响应式数据
const user = ref({
name: '张三',
age: 18
})
const updateUser = () => {
user.value.name = '李四' // 注意这里需要 .value
}
</script>
代码解释:
这段代码展示了用 ref 来处理对象类型。虽然 user 是一个对象,但我们仍然使用 ref 来创建响应式引用。访问属性时需要先 .value 再访问具体属性。
控制台中的 ref 对象
让我们看看用 ref 创建的对象在控制台中是什么样子:
TypeScript
console.log(user) // 输出: RefImpl {_value: Proxy}
深入理解:
你会发现一个有趣的现象:ref 在处理对象类型时,实际上是在内部调用了 reactive。具体来说:
-
ref创建一个RefImpl包装器 -
如果值是对象,
ref内部会调用reactive把这个对象变成 Proxy -
这个 Proxy 对象被存储在
_value属性中
这意味着 ref 完全可以替代 reactive 来定义对象类型的数据,只是在访问时需要多写一个 .value。
五、ref 与 reactive 的深度区别
虽然 ref 和 reactive 都能创建响应式数据,但它们在用法和特性上有一些重要区别。理解这些区别有助于我们在实际开发中做出更好的选择。
1. 数据类型支持
ref 可以定义基本类型和对象类型,而 reactive 只能定义对象类型。这使 ref 成为更通用的选择。
2. 访问方式
ref 需要通过 .value 访问,reactive 直接访问属性。不过现代编辑器如 VS Code 有 Vue 插件volar 可以自动补全 .value。不过需要在设置中勾选上Dot Value。
3. 重新赋值的区别
使用
reactive时,直接重新分配对象会导致响应式丢失。可以通过Object.assign()方法实现对象重新分配并保持响应式。而ref则可以直接进行重新赋值操作。
这是两者最重要的区别,让我们通过代码来理解:
TypeScript
// 使用reactive定义的响应式对象
let state = reactive({ count: 0 })
state = { count: 1 } // 重新赋值一个新对象,state失去响应式
// 解决办法,使用 Object.assign 保持响应式
Object.assign(state, { count: 1 }) // state仍然具有响应性
// 使用ref定义的响应式对象
const state = ref({ count: 0 })
// 可以直接重新赋值
state.value = { count: 1 } // state仍然具有响应性
原理深度解释:
为什么 reactive 重新赋值会失去响应式?这要从它们的实现机制说起:
-
reactive返回的是原始对象的 Proxy 包装 -
当你用新对象替换整个
state时,你实际上是把变量指向了一个新的普通对象 -
Vue 的响应式系统仍然追踪的是原来的 Proxy 对象,但你已经不再使用它了
而 ref 的机制不同:
-
ref返回的是一个包装对象,它的.value属性存储实际值 -
当你给
.value赋新值时,Vue 会检测到这是新的值,并为其创建新的响应式代理 -
因此响应式连接不会断开
使用原则总结
基于以上理解,我们可以得出一些使用原则:
-
基本类型 :必须使用
ref,因为reactive不能处理基本类型 -
对象类型:
-
如果对象结构相对简单,且不需要重新赋值,两个都可以
-
如果需要重新赋值整个对象,推荐使用
ref -
如果对象层级很深,推荐使用
ref,因为访问方式更一致 -
不必过于纠结,根据团队习惯选择即可
-
六、toRefs 和 toRef:保持响应式的解构
在 JavaScript 中,我们经常使用解构赋值来提取对象的属性,让代码更简洁。但在 Vue 中,直接解构响应式对象会导致一个常见的问题:失去响应式。
问题演示
TypeScript
const person = reactive({ name: "张三", age: 18 })
// 直接解构 - 会失去响应式!
let { name, age } = person
const changeName = () => {
name += '~' // 这不会更新原始对象
console.log(person.name) // 还是 "张三",没有变化
}
问题分析:
为什么直接解构会失去响应式?因为解构出来的 name 和 age 是普通的字符串和数字,它们只是原始值的副本。当你修改这些副本时,它们与原始的响应式对象已经没有任何关系了,Vue 自然无法追踪这些变化。
使用 toRefs 保持响应式
TypeScript
<template>
<div>
<p>姓名: {{ name }}</p>
<p>年龄: {{ age }}</p>
<button @click="changeName">修改姓名</button>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
const person = reactive({ name: "张三", age: 18 })
// 使用 toRefs 解构 - 保持响应式
let { name, age } = toRefs(person)
const changeName = () => {
name.value += '~' // 注意需要 .value
console.log(person.name) // 现在会变成 "张三~"
}
</script>
toRefs 的工作原理:
toRefs 是一个非常聪明的工具函数,它的工作方式是:
-
遍历响应式对象的所有属性
-
将每个属性转换为一个
ref对象 -
返回包含这些
ref的新对象
这样解构出来的
name和age仍然是响应式的引用,它们指向原始对象的对应属性。当你修改name.value时,实际上是在修改person.name,所以原始对象也会被更新。
toRef 的用法
toRef 用于将单个属性转换为 ref,使用场景相对较少:
TypeScript
import { reactive, toRef } from 'vue'
const person = reactive({ name: "张三", age: 18 })
// 只转换 name 属性
const nameRef = toRef(person, 'name')
// 现在可以响应式地使用 nameRef
nameRef.value = '李四'
console.log(person.name) // 输出: "李四"
使用场景:
toRef 通常在你只需要解构一个属性,或者想要为响应式对象的某个属性创建单独的引用时使用。
七、总结
通过本文的学习,相信你已经对 Vue3 的响应式系统有了深入的理解。
核心要点回顾
响应式是 Vue 的核心魔法,它通过 ref 和 reactive 让数据变化能够自动触发界面更新。ref 适合基本类型和需要重新赋值的对象,reactive 适合不需要重新赋值的对象。使用 toRefs 可以在解构时保持响应式。
下一节我们将一起探讨计算属性computed。
关于 Vue3 响应式数据有任何疑问?欢迎在评论区提出,我们会详细解答!