我喜欢Vue 3的Composition API,它提供了两种方法来为Vue组件添加响应式状态:ref
和reactive
。当你使用ref
时到处使用.value
是很麻烦的,但当你用reactive
创建的响应式对象进行重构时,也很容易丢失响应性。 在这篇文章中,我将阐释你如何来选择reactive
以及ref
。
一句话总结:默认情况下使用
ref
,当你需要对变量分组时使用reactive
。
Vue3的响应式
在我解释ref
和reactive
之前,你应该了解Vue3响应式系统的基本知识。
如果你已经掌握了Vue3响应式系统是如何工作的,你可以跳过本小节。
很不幸,JavaScript默认情况下并不是响应式的。让我们看看下面代码示例:
let price = 10.0
const quantity = 2
const total = price * quantity
console.log(total) // 20
price = 20.0
console.log(total) // ⚠️ total is still 20
在响应式系统中,我们期望每当price
或者quantity
改变时,total
就会被更新。但是JavaScript通常情况下并不会像预期的这样生效。
你也许会嘀咕,为什么Vue需要响应式系统?答案很简单:Vue 组件的状态由响应式 JavaScript 对象组成。当你修改这些对象时,视图或者依赖的响应式对象就会更新。
因此,Vue框架必须实现另一种机制来跟踪局部变量的读和写,它是通过拦截对象属性的读写来实现的。这样一来,Vue就可以跟踪一个响应式对象的属性访问以及更改。
由于浏览器的限制,Vue 2专门使用getters/setters来拦截属性。Vue 3对响应式对象使用Proxy,对ref
使用getters/setters。下面的伪代码展示了属性拦截的基本原理;它解释了核心概念,并忽略了许多细节和边缘情况:
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)
},
})
}
proxy
的get
和set
方法通常被称为代理陷阱。
这里强烈建议阅读官方文档来查看有关Vue响应式系统的更多细节。
reactive()
现在,让我们来分析下,你如何使用Vue3的reactive()
函数来声明一个响应式状态:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
该状态默认是深度响应式的。如果你修改了嵌套的数组或对象,这些更改都会被vue检测到:
import { reactive } from 'vue'
const state = reactive({
count: 0,
nested: { count: 0 },
})
watch(state, () => console.log(state))
// "{ count: 0, nested: { count: 0 } }"
const incrementNestedCount = () => {
state.nested.count += 1
// Triggers watcher -> "{ count: 0, nested: { count: 1 } }"
}
限制
reactive()
API有两个限制:
第一个限制是,它只适用于对象类型,比如对象、数组和集合类型,如Map
和Set
。它不适用于原始类型,比如string
、number
或boolean
。
第二个限制是,从reactive()
返回的代理对象与原始对象是不一样的。用===
操作符进行比较会返回false
:
const plainJsObject = {}
const proxy = reactive(plainJsObject)
// proxy is NOT equal to the original plain JS object.
console.log(proxy === plainJsObject) // false
你必须始终保持对响应式对象的相同引用,否则,Vue无法跟踪对象的属性。如果你试图将一个响应式对象的属性解构为局部变量,你可能会遇到这个问题:
const state = reactive({
count: 0,
})
// ⚠️ count is now a local variable disconnected from state.count
let { count } = state
count += 1 // ⚠️ Does not affect original state
幸运的是,你可以首先使用toRefs
将对象的所有属性转换为响应式的,然后你可以解构对象而不丢失响应:
let state = reactive({
count: 0,
})
// count is a ref, maintaining reactivity
const { count } = toRefs(state)
如果你试图重新赋值reactive
的值,也会发生类似的问题。如果你"替换"一个响应式对象,新的对象会覆盖对原始对象的引用,并且响应式连接会丢失:
const state = reactive({
count: 0,
})
watch(state, () => console.log(state), { deep: true })
// "{ count: 0 }"
// ⚠️ The above reference ({ count: 0 }) is no longer being tracked (reactivity connection is lost!)
state = reactive({
count: 10,
})
// ⚠️ The watcher doesn't fire
如果我们传递一个属性到函数中,响应式连接也会丢失:
const state = reactive({
count: 0,
})
const useFoo = (count) => {
// ⚠️ Here count is a plain number and the useFoo composable
// cannot track changes to state.count
}
useFoo(state.count)
ref()
Vue提供了ref()
函数来解决reactive()
的限制。
ref()
并不局限于对象类型,而是可以容纳任何值类型:
import { ref } from 'vue'
const count = ref(0)
const state = ref({ count: 0 })
为了读写通过ref()
创建的响应式变量,你需要通过.value
属性来访问:
const count = ref(0)
const state = ref({ count: 0 })
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
state.value.count = 1
console.log(state.value) // { count: 1 }
你可能会问自己,ref()
如何能容纳原始类型,因为我们刚刚了解到Vue需要一个对象才能触发get/set代理陷阱。下面的伪代码展示了ref()
背后的简化逻辑:
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
},
}
return refObject
}
当拥有对象类型时,ref
自动用reactive()
转换其.value
:
ref({}) ~= ref(reactive({}))
如果你想深入了解,可以在源码中查看
ref()
的实现。
不幸的是,也不能对用ref()
创建的响应式对象进行解构。这也会导致响应式丢失:
import { ref } from 'vue'
const count = ref(0)
const countValue = count.value // ⚠️ disconnects reactivity
const { value: countDestructured } = count // ⚠️ disconnects reactivity
但是,如果将ref
分组在一个普通的JavaScript对象中,就不会丢失响应式:
const state = {
count: ref(1),
name: ref('Michael'),
}
const { count, name } = state // still reactive
ref
也可以被传递到函数中而不丢失响应式。
const state = {
count: ref(1),
name: ref('Michael'),
}
const useFoo = (count) => {
/**
* The function receives a ref
* It needs to access the value via .value but it
* will retain the reactivity connection
*/
}
useFoo(state.count)
这种能力相当重要,因为它在将逻辑提取到组合式函数中时经常被使用。 一个包含对象值的ref
可以响应式地替换整个对象:
const state = {
count: 1,
name: 'Michael',
}
// Still reactive
state.value = {
count: 2,
name: 'Chris',
}
解包refs()
在使用ref
时到处使用.value
可能很麻烦,但我们可以使用一些辅助函数。
unref实用函数
unref()是一个便捷的实用函数,在你的值可能是一个ref
的情况下特别有用。在一个非ref
上调用.value
会抛出一个运行时错误,unref()
在这种情况下就很有用:
import { ref, unref } from 'vue'
const count = ref(0)
const unwrappedCount = unref(count)
// same as isRef(count) ? count.value : count`
如果unref()
的参数是一个ref
,就会返回其内部值。否则就返回参数本身。这是的val = isRef(val) ? val.value : val
语法糖。
模板解包
当你在模板上调用ref
时,Vue会自动使用unref()
进行解包。这样,你永远不需要在模板中使用.value
进行访问:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<span>
<!-- no .value needed -->
{{ count }}
</span>
</template>
只在
ref
是模板中的顶级属性时才生效。
侦听器
我们可以直接传递一个ref
作为侦听器的依赖:
import { watch, ref } from 'vue'
const count = ref(0)
// Vue automatically unwraps this ref for us
watch(count, (newCount) => console.log(newCount))
Volar
如果你正在使用VS Code,你可以通过配置Volar扩展来自动地添加.value
到ref
上。你可以在Volar: Auto Complete Refs
设置中开启:
相应的JSON设置:
"volar.autoCompleteRefs": true
为了减少CPU的使用,这个功能默认是禁用的。
比较
让我们总结一下reactive
和ref
之间的区别:
reactive | ref |
---|---|
标签:count,const,reactive,Ref,value,Reactive,state,使用,ref
From: https://www.cnblogs.com/chuckQu/p/17350975.html
相关文章
|