全方位带你掌握 ref、reactive,开启 Vue3 响应式的大门
ztj100 2025-04-30 21:22 27 浏览 0 评论
不知道大家使用 Vue3 的时候有没有这样的疑惑,“ref、rective 都能创建一个响应式对象,我该如何选择?”,“为什么响应式对象解构之后就失去了响应式?应该如何处理?” 今天咱们就来全面盘点一下 ref、reactive,相信看完所有响应式问题都会迎刃而解,一起学起来吧!
reactive()
基本用法
在 Vue3 中我们可以使用 reactive() 创建一个响应式对象或数组:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
这个响应式对象其实就是一个 Proxy, Vue 会在这个 Proxy 的属性被访问时收集副作用,属性被修改时触发副作用。
要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({ count: 0 })
return {
state
}
}
}
</script>
<template>
<div>{{ state.count }}</div>
</template>
当然,也可以使用 <script setup> ,<script setup> 中顶层的导入和变量声明可以在模板中直接使用。
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
</script>
<template>
<div>{{ state.count }}</div>
</template>
响应式代理 vs 原始对象
reactive() 返回的是一个原始对象的 Proxy,他们是不相等的:
const raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) // false
原始对象在模板中也是可以使用的,但修改原始对象不会触发更新。因此,要使用 Vue 的响应式系统,就必须使用代理。
<script setup>
const state = { count: 0 }
function add() {
state.count++
}
</script>
<template>
<button @click="add">
{{ state.count }} <!-- 当点击button时,始终显示为 0 -->
</button>
</template>
为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:
const raw = {}
const proxy1 = reactive(raw)
const proxy2 = reactive(raw)
console.log(proxy1 === proxy2) // true
console.log(reactive(proxy1) === proxy1) // true
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
const raw = {}
const proxy = reactive({ nested: raw })
const nested = reactive(raw)
console.log(proxy.nested === nested) // true
shallowReactive()
在 Vue 中,状态默认都是深层响应式的。但某些场景下,我们可能想创建一个 浅层响应式对象 ,让它仅在顶层具有响应性,这时候可以使用 shallowReactive()。
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 状态自身的属性是响应式的
state.foo++
// 下层嵌套对象不是响应式的,不会按期望工作
state.nested.bar++
注意:浅层响应式对象应该只用于组件中的根级状态。避免将其嵌套在深层次的响应式对象中,因为其内部的属性具有不一致的响应行为,嵌套之后将很难理解和调试。
reactive() 的局限性
reactive() 虽然强大,但也有以下几条限制:
1、 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的原始类型无效。
2、因为 Vue 的响应式系统是通过属性访问进行追踪的,如果我们直接“替换”一个响应式对象,这会导致对初始引用的响应性连接丢失:
<script setup>
import { reactive } from 'vue'
let state = reactive({ count: 0 })
function change() {
// 非响应式替换
state = reactive({ count: 1 })
}
</script>
<template>
<button @click="change">
{{ state }} <!-- 当点击button时,始终显示为 { "count": 0 } -->
</button>
</template>
3、将响应式对象的属性赋值或解构至本地变量,或是将该属性传入一个函数时,会失去响应性:
const state = reactive({ count: 0 })
// n 是一个局部变量,和 state.count 失去响应性连接
let n = state.count
// 不会影响 state
n++
// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响 state
count++
// 参数 count 同样和 state.count 失去了响应性连接
function callSomeFunction(count) {
// 不会影响 state
count++
}
callSomeFunction(state.count)
为了解决以上几个限制,ref 闪耀登场了!
ref()
Vue 提供了一个 ref() 方法来允许我们创建使用任何值类型的响应式 ref 。
基本用法
ref() 将传入的参数包装为一个带有 value 属性的 ref 对象:
import { ref } from 'vue'
const count = ref(0)
console.log(count) // { value: 0 }
count.value++
console.log(count.value) // 1
和响应式对象的属性类似,ref 的 value 属性也是响应式的。同时,当值为对象类型时,Vue 会自动使用 reactive() 处理这个值。
一个包含对象的 ref 可以响应式地替换整个对象:
<script setup>
import { ref } from 'vue'
let state = ref({ count: 0 })
function change() {
// 这是响应式替换
state.value = ref({ count: 1 })
}
</script>
<template>
<button @click="change">
{{ state }} <!-- 当点击button时,显示为 { "count": 1 } -->
</button>
</template>
ref 从一般对象上解构属性或将属性传递给函数时,不会丢失响应性:
const state = {
count: ref(0)
}
// 解构之后,和 state.count 依然保持响应性连接
const { count } = state
// 会影响 state
count.value++
// 该函数接收一个 ref, 和传入的值保持响应性连接
function callSomeFunction(count) {
// 会影响 state
count.value++
}
callSomeFunction(state.count)
ref() 让我们能创建使用任何值类型的 ref 对象,并能够在不丢失响应性的前提下传递这些对象。这个功能非常重要,经常用于将逻辑提取到 组合式函数 中。
// mouse.js
export function useMouse() {
const x = ref(0)
const y = ref(0)
// ...
return { x, y }
}
<script setup>
import { useMouse } from './mouse.js'
// 可以解构而不会失去响应性
const { x, y } = useMouse()
</script>
ref 的解包
所谓解包就是获取到 ref 对象上 value 属性的值。常用的两种方法就是 .value 和 unref()。 unref() 是 Vue 提供的方法,如果参数是 ref ,则返回 value 属性的值,否则返回参数本身。
1、ref 在模板中的解包
当 ref 在模板中作为顶层属性被访问时,它们会被自动解包,不需要使用 .value 。下面是之前的例子,使用 ref() 代替:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div>
{{ count }} <!-- 无需 .value -->
</div>
</template>
还有一种情况,如果文本插值({{ }})计算的最终值是 ref ,也会被自动解包。下面的非顶层属性会被正确渲染出来。
<script setup>
import { ref } from 'vue'
const object = { foo: ref(1) }
</script>
<template>
<div>
{{ object.foo }} <!-- 无需 .value -->
</div>
</template>
其他情况则不会被自动解包,如:object.foo 不是顶层属性,文本插值({{ }})计算的最终值也不是 ref:
const object = { foo: ref(1) }
下面的内容将不会像预期的那样工作:
<div>{{ object.foo + 1 }}</div>
渲染的结果会是 [object Object]1,因为 object.foo 是一个 ref 对象。我们可以通过将 foo 改成顶层属性来解决这个问题:
const object = { foo: ref(1) }
const { foo } = object
<div>{{ foo + 1 }}</div>
现在结果就可以正确地渲染出来了。
2、ref 在响应式对象中的解包
当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现得和一般的属性一样:
const count = ref(0)
const state = reactive({ count })
console.log(state.count) // 0
state.count = 1
console.log(state.count) // 1
只有当嵌套在一个深层响应式对象内时,才会发生解包。当 ref 作为 浅层响应式对象 的属性被访问时则不会解包:
const count = ref(0)
const state = shallowReactive({ count })
console.log(state.count) // { value: 0 } 而不是 0
如果将一个新的 ref 赋值给一个已经关联 ref 的属性,那么它会替换掉旧的 ref:
const count = ref(1)
const state = reactive({ count })
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 此时 count 已经和 state.count 失去连接
console.log(count.value) // 1
3、ref 在数组和集合类型的解包
跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
toRef()
toRef 是基于响应式对象上的一个属性,创建一个对应的 ref 的方法。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
// 更改源属性会更新该 ref
state.foo++
console.log(fooRef.value) // 2
// 更改该 ref 也会更新源属性
fooRef.value++
console.log(state.foo) // 3
toRef() 在你想把一个 prop 的 ref 传递给一个组合式函数时会很有用:
<script setup>
import { toRef } from 'vue'
const props = defineProps(/* ... */)
// 将 `props.foo` 转换为 ref,然后传入一个组合式函数
useSomeFeature(toRef(props, 'foo'))
</script>
当 toRef 与组件 props 结合使用时,关于禁止对 props 做出更改的限制依然有效。如果将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。在这种场景下,你可以考虑使用带有 get 和 set 的 computed 替代。
注意:即使源属性当前不存在,toRef() 也会返回一个可用的 ref。这让它在处理可选 props 的时候非常有用,相比之下 toRefs 就不会为可选 props 创建对应的 refs 。下面我们就来了解一下 toRefs 。
toRefs()
toRefs() 是将一个响应式对象上的所有属性都转为 ref ,然后再将这些 ref 组合为一个普通对象的方法。这个普通对象的每个属性和源对象的属性保持同步。
const state = reactive({
foo: 1,
bar: 2
})
// 相当于
// const stateAsRefs = {
// foo: toRef(state, 'foo'),
// bar: toRef(state, 'bar')
// }
const stateAsRefs = toRefs(state)
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
从组合式函数中返回响应式对象时,toRefs 相当有用。它可以使我们解构返回的对象时,不失去响应性:
// feature.js
export function useFeature() {
const state = reactive({
foo: 1,
bar: 2
})
// ...
// 返回时将属性都转为 ref
return toRefs(state)
}
<script setup>
import { useFeature } from './feature.js'
// 可以解构而不会失去响应性
const { foo, bar } = useFeature()
</script>
toRefs 只会为源对象上已存在的属性创建 ref。如果要为还不存在的属性创建 ref,就要用到上面提到的 toRef。
以上就是 ref、reactive 的详细用法,不知道你有没有新的收获。接下来,我们来探讨一下响应式原理。
响应式原理
Vue2 的限制
大家都知道 Vue2 中的响应式是采用 Object.defineProperty() , 通过 getter / setter 进行属性的拦截。这种方式对旧版本浏览器的支持更加友好,但它有众多缺点:
- 初始化时只会对已存在的对象属性进行响应式处理。也是说新增或删除属性,Vue 是监听不到的。必须使用特殊的 API 处理。
- 数组是通过覆盖原型对象上的7个方法进行实现。如果通过下标去修改数据,Vue 同样是无法感知的。也要使用特殊的 API 处理。
- 无法处理像 Map、 Set 这样的集合类型。
- 带有响应式状态的逻辑不方便复用。
Vue3 的响应式系统
针对上述情况,Vue3 的响应式系统横空出世了!Vue3 使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref ,完美的解决了上述几条限制。下面的代码可以说明它们是如何工作的:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
不难看出,当将一个响应性对象的属性解构为一个局部变量时,响应性就会“断开连接”。因为对局部变量的访问不会触发 get / set 代理捕获。
我们回到响应式原理。在 track() 内部,我们会检查当前是否有正在运行的副作用。如果有,就会查找到存储了所有追踪了该属性的订阅者的 Set,然后将当前这个副作用作为新订阅者添加到该 Set 中。
// activeEffect 会在一个副作用就要运行之前被设置
let activeEffect
function track(target, key) {
if (activeEffect) {
const effects = getSubscribersForProperty(target, key)
effects.add(activeEffect)
}
}
副作用订阅将被存储在一个全局的 WeakMap<target, Map<key, Set<effect>>> 数据结构中。如果在第一次追踪时没有找到对相应属性订阅的副作用集合,它将会在这里新建。这就是 getSubscribersForProperty() 函数所做的事。
在 trigger() 之中,我们会再次查找到该属性的所有订阅副作用。这一次我们全部执行它们:
function trigger(target, key) {
const effects = getSubscribersForProperty(target, key)
effects.forEach((effect) => effect())
}
这些副作用就是用来执行 diff 算法,从而更新页面的。
这就是响应式系统的大致原理,Vue3 还做了编译器的优化,diff 算法的优化等等。不得不佩服尤大大,把 Vue 的响应式系统又提升了一个台阶!
ok,今天的分享就到这里,不知道你对 ref、reactive 是不是有了更加深入的了解呢,欢迎在评论区留言。如果觉得有点,记得点赞支持一哈!
参考文档:Vue3 官网
相关推荐
- 30天学会Python编程:16. Python常用标准库使用教程
-
16.1collections模块16.1.1高级数据结构16.1.2示例...
- 强烈推荐!Python 这个宝藏库 re 正则匹配
-
Python的re模块(RegularExpression正则表达式)提供各种正则表达式的匹配操作。...
- Python爬虫中正则表达式的用法,只讲如何应用,不讲原理
-
Python爬虫:正则的用法(非原理)。大家好,这节课给大家讲正则的实际用法,不讲原理,通俗易懂的讲如何用正则抓取内容。·导入re库,这里是需要从html这段字符串中提取出中间的那几个文字。实例一个对...
- Python数据分析实战-正则提取文本的URL网址和邮箱(源码和效果)
-
实现功能:Python数据分析实战-利用正则表达式提取文本中的URL网址和邮箱...
- python爬虫教程之爬取当当网 Top 500 本五星好评书籍
-
我们使用requests和re来写一个爬虫作为一个爱看书的你(说的跟真的似的)怎么能发现好书呢?所以我们爬取当当网的前500本好五星评书籍怎么样?ok接下来就是学习python的正确姿...
- 深入理解re模块:Python中的正则表达式神器解析
-
在Python中,"re"是一个强大的模块,用于处理正则表达式(regularexpressions)。正则表达式是一种强大的文本模式匹配工具,用于在字符串中查找、替换或提取特定模式...
- 如何使用正则表达式和 Python 匹配不以模式开头的字符串
-
需要在Python中使用正则表达式来匹配不以给定模式开头的字符串吗?如果是这样,你可以使用下面的语法来查找所有的字符串,除了那些不以https开始的字符串。r"^(?!https).*&...
- 先Mark后用!8分钟读懂 Python 性能优化
-
从本文总结了Python开发时,遇到的性能优化问题的定位和解决。概述:性能优化的原则——优化需要优化的部分。性能优化的一般步骤:首先,让你的程序跑起来结果一切正常。然后,运行这个结果正常的代码,看看它...
- Python“三步”即可爬取,毋庸置疑
-
声明:本实例仅供学习,切忌遵守robots协议,请不要使用多线程等方式频繁访问网站。#第一步导入模块importreimportrequests#第二步获取你想爬取的网页地址,发送请求,获取网页内...
- 简单学Python——re库(正则表达式)2(split、findall、和sub)
-
1、split():分割字符串,返回列表语法:re.split('分隔符','目标字符串')例如:importrere.split(',','...
- Lavazza拉瓦萨再度牵手上海大师赛
-
阅读此文前,麻烦您点击一下“关注”,方便您进行讨论和分享。Lavazza拉瓦萨再度牵手上海大师赛标题:2024上海大师赛:网球与咖啡的浪漫邂逅在2024年的上海劳力士大师赛上,拉瓦萨咖啡再次成为官...
- ArkUI-X构建Android平台AAR及使用
-
本教程主要讲述如何利用ArkUI-XSDK完成AndroidAAR开发,实现基于ArkTS的声明式开发范式在android平台显示。包括:1.跨平台Library工程开发介绍...
- Deepseek写歌详细教程(怎样用deepseek写歌功能)
-
以下为结合DeepSeek及相关工具实现AI写歌的详细教程,涵盖作词、作曲、演唱全流程:一、核心流程三步法1.AI生成歌词-打开DeepSeek(网页/APP/API),使用结构化提示词生成歌词:...
- “AI说唱解说影视”走红,“零基础入行”靠谱吗?本报记者实测
-
“手里翻找冻鱼,精心的布局;老漠却不言语,脸上带笑意……”《狂飙》剧情被写成歌词,再配上“科目三”背景音乐的演唱,这段1分钟30秒的视频受到了无数网友的点赞。最近一段时间随着AI技术的发展,说唱解说影...
- AI音乐制作神器揭秘!3款工具让你秒变高手
-
在音乐创作的领域里,每个人都有一颗想要成为大师的心。但是面对复杂的乐理知识和繁复的制作过程,许多人的热情被一点点消磨。...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 30天学会Python编程:16. Python常用标准库使用教程
- 强烈推荐!Python 这个宝藏库 re 正则匹配
- Python爬虫中正则表达式的用法,只讲如何应用,不讲原理
- Python数据分析实战-正则提取文本的URL网址和邮箱(源码和效果)
- python爬虫教程之爬取当当网 Top 500 本五星好评书籍
- 深入理解re模块:Python中的正则表达式神器解析
- 如何使用正则表达式和 Python 匹配不以模式开头的字符串
- 先Mark后用!8分钟读懂 Python 性能优化
- Python“三步”即可爬取,毋庸置疑
- 简单学Python——re库(正则表达式)2(split、findall、和sub)
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)