Vue3 中应该使用 Ref 还是 Reactive?

为什么要使用 ref?

你可能会好奇:为什么我们需要使用带有 .value 的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue 的响应式系统是如何工作的。

当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。

在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。

该 .value 属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:

// 伪代码,不是真正的实现
const myRef = {_value: 0,get value() {track()return this._value},set value(newValue) {this._value = newValuetrigger()}
}

另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。

该响应性系统在vue官网深入响应式原理章节中有更详细的讨论。

深层响应性

Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map

Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:

import { ref } from 'vue'const obj = ref({nested: { count: 0 },arr: ['foo', 'bar']
})function mutateDeeply() {// 以下都会按照期望工作obj.value.nested.count++obj.value.arr.push('baz')
}

非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。

也可以通过shallow ref来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。

DOM 更新时机 

当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。

要等待 DOM 更新完成后再执行额外的代码,可以使用nextTick()全局 API:

import { nextTick } from 'vue'async function increment() {count.value++await nextTick()// 现在 DOM 已经更新了
}

reactive()

还有另一种声明响应式状态的方式,即使用 reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:

import { reactive } from 'vue'const state = reactive({ count: 0 })

值得注意的是,reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:

const raw = {}
const proxy = reactive(raw)// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false

只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本

为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:

// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:

const proxy = reactive({})const raw = {}
proxy.nested = rawconsole.log(proxy.nested === raw) // false

reactive() 的局限性

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumber 或 boolean 这样的原始类型。

  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:

    let state = reactive({ count: 0 })// 上面的 ({ count: 0 }) 引用将不再被追踪
    // (响应性连接已丢失!)
    state = reactive({ count: 1 })
  3. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:

     
    const state = reactive({ count: 0 })// 当解构时,count 已经与 state.count 断开连接
    let { count } = state
    // 不会影响原始的 state
    count++// 该函数接收到的是一个普通的数字
    // 并且无法追踪 state.count 的变化
    // 我们必须传入整个对象以保持响应性
    callSomeFunction(state.count)

由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。

为什么推荐使用ref而不是reactive呢?

  1. 局限性问题: reactive本身存在一些局限性,可能会在开发过程中引发一些问题。这需要额外的注意力和处理,否则可能对开发造成麻烦。

  2. 数据类型限制: reactive声明的数据类型仅限于对象,而ref则更加灵活,可以容纳任何数据类型。这使得ref更适合一般的响应式状态的声明。

  3. 官方推荐: 官方文档强烈建议使用ref()作为声明响应式状态的首选。这是因为ref更简单、更直观,同时避免了reactive可能引发的一些问题。

总的来说:除非有特定的需求需要使用reactive,否则在大多数情况下更推荐使用ref()

reactive和 ref 对比

reactiveref
❌ 只支持对象和数组(引用数据类型)✅ 支持基本数据类型 + 引用数据类型
✅ 在 <script> 和 <template> 中无差别使用❌ 在 <script> 和 <template> 使用方式不同(在 <script> 中要使用 .value
❌ 重新分配一个新对象会丢失响应性✅ 重新分配一个新对象不会失去响应
能直接访问属性需要使用 .value 访问属性
❌ 将对象传入函数时,失去响应✅ 传入函数时,不会失去响应
❌ 解构时会丢失响应性,需使用 toRefs❌ 解构对象时会丢失响应性,需使用 toRefs

即:

  • ref 用于将基本类型的数据和引用数据类型(对象)转换为响应式数据,通过 .value 访问和修改。

  • reactive 用于将对象转换为响应式数据,可以直接访问和修改属性,适用于复杂的嵌套对象和数组。

01: reactive 有限的值类型

reactive 只能声明引用数据类型(对象)

let obj = reactive({name: '小明',age: 18
})

ref 既能声明基本数据类型,也能声明对象和数组

Vue 提供了 ref() 方法,允许我们创建可以使用任何值类型的响应式 ref

// 对象
const state = ref({})
// 数组
const state2 = ref([])

使用 ref,你可以灵活地声明基本数据类型、对象或数组,而不受像 reactive 那样只能处理引用数据类型的限制。这为开发提供了更大的灵活性,尤其是在处理不同类型的数据时。

02: reactive 使用不当会失去响应

使用 reactive 时,如果不当使用,可能导致响应性失效,带来一些困扰。这可能让开发者在愉快编码的同时,突然发现某些操作失去了响应性,不明所以。因此,建议在不了解 reactive 失去响应的情况下慎用,而更推荐使用 ref

1. 赋值给 reactive 一个整个对象或 reactive 对象

赋值一个普通对象
let state = reactive({ count: 0 })
// 这个赋值将导致 state 失去响应
state = { count: 1 }
赋值一个 reactive 对象
<template>{{ state }}
</template>    <script setup>
const state = reactive({ count: 0 })
// 在 nextTick 异步方法中修改 state 的值
nextTick(() => {// 并不会触发修改 DOM ,说明失去响应了state = reactive({ count: 11 });
});
</script>

在 nextTick 中给 state 赋值一个 reactive 的响应式对象,但是 DOM 并没有更新。

解决方法:

  1. 不要直接整个对象替换,一个个属性赋值

let state = reactive({ count: 0 })
// state = { count: 1 }
state.count = 1
  1. 使用 Object.assign

let state = reactive({ count: 0 })
// state = { count: 1 },state 不会失去响应
state = Object.assign(state, { count: 1 })
  1. 使用 ref 定义对象

let state = ref({ count: 0 })
state.value = { count: 1 }

2. 将 reactive 对象的属性赋值给变量(断开连接/深拷贝)

这种操作类似于深拷贝,不再共享同一内存地址,而是只是字面量的赋值,对该变量的赋值不会影响原来对象的属性值。

let state = reactive({ count: 0 })
// 赋值给 n,n 和 state.count 不再共享响应性连接
let n = state.count
// 不影响原始的 state
n++
console.log(state.count) // 0

解决方案:

  • 避免将 reactive 对象的属性赋值给变量。

3. 直接 reactive 对象解构时

直接解构会失去响应。

let state = reactive({ count: 0 })
// 普通解构,count 和 state.count 失去了响应性连接
let { count } = state
count++ // state.count 值依旧是 0

解决方案:

使用 toRefs 解构,解构后的属性是 ref 的响应式变量。

const state = reactive({ count: 0 })
// 使用 toRefs 解构,后的属性为 ref 的响应式变量
let { count } = toRefs(state)
count.value++ // state.count 值改变为 1

建议:ref 一把梭

推荐使用 ref,总结原因如下:

  1. reactive 有限的值类型:只能声明引用数据类型(对象/数组)。

  2. reactive 在一些情况下会失去响应,这可能导致数据回显失去响应(数据改了,DOM 没更新)。

   <template>{{ state.a }}{{ state.b }}{{ state.c }}</template><script>let state = reactive({ a: 1, b: 2, c: 3 })onMounted(() => {// 通过 AJAX 请求获取的数据,回显到 reactive,如果处理不好将导致变量失去响应// 回显失败,给响应式数据赋值一个普通对象state = { a: 11, b: 22, c: 333 }// 回显成功,一个个属性赋值state.a = 11state.b = 22state.c = 33})</script>

上面这个例子如果是使用 ref 进行声明,直接赋值即可,不需要将属性拆分一个个赋值。

使用 ref 替代 reactive

   <template>{{ state.a }}{{ state.b }}{{ state.c }}</template><script>let state = ref({ a: 1, b: 2, c: 3 })onMounted(() => {// 回显成功state.value = { a: 11, b: 22, c: 333 }})</script>
    • 给响应式对象的字面量赋一整个普通对象或 reactive 对象将导致 reactive 声明的响应式数据失去响应。

  1. ref 适用范围更广,可声明基本数据类型和引用数据类型。

虽然使用 ref 声明的变量在读取和修改时都需要加 .value 小尾巴,但正因为有这个小尾巴,我们在 review 代码的时候就很清楚知道这是一个 ref 声明的响应式数据。

ref 的 .value 好麻烦!

ref 声明的响应式变量携带迷人的 .value 小尾巴,让我们一眼就能确定它是一个响应式变量。虽然使用 ref 声明的变量在读取和修改时都需要加 .value 小尾巴,但是正因为有这个小尾巴,我们在 review 代码的时候就很清楚知道这是一个 ref 声明的响应式数据。

Volar 插件能自动补全 .value

推荐 ref 一把梭,但是 ref 又得到处 .value,那就交给插件来完成吧!

  • Volar 自动补全 .value(不是默认开启,需要手动开启)

reactive 重新赋值丢失响应是因为引用地址变了,被 proxy 代理的对象已经不是原来的那个,所以丢失响应了。其实 ref 也是一样的,当把 .value 那一层替换成另外一个有着 .value 的对象也会丢失响应。ref 定义的属性等价于 reactive({ value: xxx })

另外,说使用 Object.assign 为什么可以更新模板:

Object.assign 解释是这样的:如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。

那个解决方法里不用重新赋值,直接 Object.assign(state, { count: 1 }) 即可,所以只要 proxy 代理的引用地址没变,就会一直存在响应性

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/684549.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

抽象的问题1

vue3&#xff0c;在使用v-mode绑定属性时&#xff0c;发生了奇怪的问题&#xff0c;渲染失败了 代码如下 <template><div><form><div>账号<input v-model"form_user_Data.username" type"text"></div><div>密…

【Python--Web应用框架大比较】

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;Python &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; Django Django太重了&#xff0c;除了web框架&#xff0c;自带ORM和模板引擎&#xff0c;灵活和自由度不…

键盘快捷切换K线周期的设计与实现

文章目录 一、键盘实现切换K周期原理二、键盘切换K线周期的代码实现 一、键盘实现切换K周期原理 首先&#xff0c;需要定义一组按键对于一组K线周期&#xff0c;按下1代表M1&#xff0c;按下2代表M5&#xff0c;以此类推。 接下来&#xff0c;需要编写一个函数来处理键盘快捷键…

[Vue的组件通讯.sync修饰]Vue中.sync的使用方法和实现的方式 代码注释

目录 .sync的使用方法1. 在父组件中&#xff0c;将需要传递给子组件的数据使用v-bind绑定到子组件的props中&#xff0c;并在属性名后加上.sync修饰符&#xff0c;如下所示&#xff1a;2. 在子组件中&#xff0c;将需要传递给父组件的数据使用$emit方法触发一个名为update:valu…

nba2k24 丁彦雨航面补

nba2k24 丁彦雨航面补 nba2k23-nba2k24通用 丁彦雨航面补 下载地址&#xff1a; https://www.changyouzuhao.cn/9528.html

free pascal:fpwebview 组件通过 JSBridge 调用本机TTS

从 https://github.com/PierceNg/fpwebview 下载 fpwebview-master.zip 简单易用。 先请看 \fpwebview-master\README.md cd \lazarus\projects\fpwebview-master\demo\js_bidir 学习 js_bidir.lpr &#xff0c;编写 js_bind_speak.lpr 如下&#xff0c;通过 JSBridge 调用本…

vue3 中使用pinia 数据状态管理(在Taro 京东移动端框架中的使用)

1.pinia 介绍 pinia 是 Vue 的存储库&#xff0c;它允许您跨组件/页面共享状态。就是和vuex一样的实现数据共享。 依据Pinia官方文档&#xff0c;Pinia是2019年由vue.js官方成员重新设计的新一代状态管理器&#xff0c;更替Vuex4成为Vuex5。 Pinia 目前也已经是 vue 官方正式的…

【自然语言处理】:实验1布置,Word2VecTranE的实现

清华大学驭风计划 因为篇幅原因实验答案分开上传&#xff0c;答案链接http://t.csdnimg.cn/5cyMG 如果需要详细的实验报告或者代码可以私聊博主 有任何疑问或者问题&#xff0c;也欢迎私信博主&#xff0c;大家可以相互讨论交流哟~~ 实验1&#xff1a; Word2Vec&TranE的…

模拟算法总结(Java)

目录 模拟算法概述 练习 练习1&#xff1a;替换所有的问号 练习2&#xff1a;提莫攻击 练习3&#xff1a;Z字形变换 模拟算法概述 模拟&#xff1a;根据题目要求的实现过程进行编程模拟&#xff0c;即题目要求什么就实现什么 解决这类题目&#xff0c;需要&#xff1a; 1…

猫头虎分享已解决Bug ‍ || Rust Error: the trait bound is not satisfied

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

操作字符串之子串替换-15-${string/%substring/replacement}

1.${string/%substring/replacement} 如果$substring匹配$string的结尾部分&#xff0c;那么就用$replacement来替换$substring 2.实例 操作字符串样例&#xff1a;stringabc123ABC456xyzabc 字符串操作默认从右边开始进行 命令&#xff1a; echo ${string/%abc/ZTJ} [r…

C语言学习day15:数组定义的格式

数组的写法格式有很多种 int arr1[6] { 1,2,3,4,5,6 }; int arr[] { 1,2,3,4,5,6 }; int arr[10] { 1,2,3,4,5 }; int arr[10]; arr[0] 1; 这些都有差别 代码&#xff1a; int main() {//int arr1[6] { 1,2,3,4,5,6 };//int arr[] { 1,2,3,4,5,6 };//int arr[10]…

部门协作、沟通壁垒、上下级偏差……组织内部如何沟通?

工作中最常遇到的问题就是沟通。 如何能在最短的时间做到令对方明白您的目的&#xff1f; 彼此确认好双方的需求&#xff1f; 确保大家都明确任务最终想要达成的效果&#xff1f; 这需要极强的沟通和协作能力&#xff0c;而高效沟通几乎是现下每个团队的管理盲点。 团队沟通是…

Innodb下修改事务工作流程(buffer pool、redo log、undolog)

1、在Buffer Pool中读取数据&#xff1a;当InnoDB需要更新一条记录时&#xff0c;首先会在Buffer Pool中查找该记录是否在内存中。如果没有在内存中&#xff0c;则从磁盘读取该页到Buffer Pool中。 2、记录UndoLog&#xff1a;在修改操作前&#xff0c;InnoDB会在Undo Log中记…

(2024,DiS,扩散,状态空间主干,Mamba)具有状态空间主干的可扩展扩散模型

Scalable Diffusion Models with State Space Backbone 公和众和号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 目录 0. 摘要 2. 方法 2.1 基础 2.2 模型结构设计 3. 实验 0. 摘要 这篇论文提出…

超详细的介绍Python语句

一、 常用命令 在介绍Python语句之前&#xff0c;先介绍一下几个有用的Python命令。 dir(模块名或类名或变量名或表达式名)&#xff1a;获得当前模块、变量对应类型、表达式计算值对应类的属性列表 type&#xff08;变量名或表达式名&#xff09;:获取变量或表达式计算值的对…

Java学习第十四节之冒泡排序

冒泡排序 package array;import java.util.Arrays;//冒泡排序 //1.比较数组中&#xff0c;两个相邻的元素&#xff0c;如果第一个数比第二个数大&#xff0c;我们就交换他们的位置 //2.每一次比较&#xff0c;都会产生出一个最大&#xff0c;或者最小的数字 //3.下一轮则可以少…

三、数据类型

数据类型 一、整型二、 浮点型三、Decimal四、布尔型五、字符串六、枚举类型七、时间类型1.Date类型2.DateTime类型 八、数组九、其他数据类型十、默认值 一、整型 固定长度的整型有两种&#xff1a; 有符号整型&#xff08;-2n-1~2n-1-1&#xff09; 使用场景&#xff1a; 个…

进程状态

广义概念&#xff1a; 从广义上来讲&#xff0c;进程分为新建、运行、阻塞、挂起、退出五个状态&#xff0c;其中新建和退出两个状态可以直接理解字面意思。 运行状态&#xff1a; 这里涉及到运行队列的概念&#xff0c;CPU在读取数据的时候&#xff0c;需要把内存中的进程放入…

Word docx文件重命名为zip文件,解压后直接查看和编辑

一个不知道算不算冷的知识[doge]&#xff1a; docx格式的文件本质上是一个ZIP文件 当把一个.docx文件重命名为.zip文件并解压后&#xff0c;你会发现里面包含了一些XML文件和媒体文件&#xff0c;它们共同构成了Word文档的内容和格式。 例如&#xff0c;word/document.xml文件…