为什么要使用 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) // falsereactive() 的局限性
 
-  有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set这样的集合类型)。它不能持有如string、number或boolean这样的原始类型。
-  不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失: let state = reactive({ count: 0 })// 上面的 ({ count: 0 }) 引用将不再被追踪 // (响应性连接已丢失!) state = reactive({ count: 1 })
-  对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接: const state = reactive({ count: 0 })// 当解构时,count 已经与 state.count 断开连接 let { count } = state // 不会影响原始的 state count++// 该函数接收到的是一个普通的数字 // 并且无法追踪 state.count 的变化 // 我们必须传入整个对象以保持响应性 callSomeFunction(state.count)
由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。
为什么推荐使用ref而不是reactive呢?
-  局限性问题: reactive本身存在一些局限性,可能会在开发过程中引发一些问题。这需要额外的注意力和处理,否则可能对开发造成麻烦。
-  数据类型限制: reactive声明的数据类型仅限于对象,而ref则更加灵活,可以容纳任何数据类型。这使得ref更适合一般的响应式状态的声明。
-  官方推荐: 官方文档强烈建议使用 ref()作为声明响应式状态的首选。这是因为ref更简单、更直观,同时避免了reactive可能引发的一些问题。
总的来说:除非有特定的需求需要使用reactive,否则在大多数情况下更推荐使用ref()。
reactive和 ref 对比
 
| reactive | ref | 
|---|---|
| ❌ 只支持对象和数组(引用数据类型) | ✅ 支持基本数据类型 + 引用数据类型 | 
| ✅ 在 <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 并没有更新。
解决方法:
-  不要直接整个对象替换,一个个属性赋值 
let state = reactive({ count: 0 })
// state = { count: 1 }
state.count = 1
-  使用 Object.assign
let state = reactive({ count: 0 })
// state = { count: 1 },state 不会失去响应
state = Object.assign(state, { count: 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,总结原因如下:
-  reactive有限的值类型:只能声明引用数据类型(对象/数组)。
-  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声明的响应式数据失去响应。
 
-  
-  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代理的引用地址没变,就会一直存在响应性