原文地址:Ref vs. Reactive: What to Choose Using Vue 3 Composition API?
官网文档相干章节:响应式根底 、深刻响应式零碎
本文参考官网文档联合集体了解做了局部批改,不足之处恳请批评指教!
我喜爱 Vue3 的组合式 API,然而它提供了两种响应式 state 办法:ref
和 reactive
。应用 refs 时到处须要 .value
显得很轻便,然而应用 reactive
又会很容易在解构时失落响应式。
在这篇文章中,我将解释该如何抉择应用 reactive
, ref
或者两者搭配应用。
太长不看版:默认应用ref
,在须要分组应用时抉择reactive
。
Vue3 中的响应式
在解释 ref
和 reactive
之前,须要先简略理解一下 Vue3 中的响应式根底。
提醒
如果你曾经理解了 Vue3 中的响应式原理,能够跳过这一章节
原生 JavaScript 并没有提供任何响应式机制,来看上面这个例子:
let price = 10.0const quantity = 2const total = price * quantityconsole.log(total) // 20price = 20.0console.log(total) // ⚠️ 后果仍旧是 20
在响应式零碎中,咱们冀望 total
在 price
或 quantity
扭转时更新。然而 JavaScript 通常不会这样做。
你可能会问本人,为什么 Vue 须要一个响应式零碎?这个答案很简略:Vue 组件的状态由响应式 JavaScript 对象组成。当你批改它们的时候,视图 view 或依赖它们的对象就会被更新。
因而,Vue 须要实现另一种机制来跟踪局部变量的读写,并且它是通过拦挡对象属性的读和写来实现的。这样,Vue 能够跟踪响应式对象属性的拜访和更改。
因为浏览器的限度,Vue2 应用 getter/setter 来拦挡属性的读写。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}
这里和上面的代码都将以最简略的模式解释外围概念,因而省略了许多细节和边缘状况。
对于 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 // 触发 watcher -> "{ count: 0, nested: { count: 1 } }"}
reactive() 的限度
reactive()
API 有两个限度:
- 仅对对象类型无效(对象、数组和
Map
、Set
这样的汇合类型),而对string
、number
和boolean
这样的根本类型有效。 - 通过
reactive()
包装后的对象与原始对象援用地址不同,应用===
操作符比拟时会返回false
:
const plainJsObject = {}const proxy = reactive(plainJsObject)// proxy 与原始 js 对象不相等console.log(proxy === plainJsObject) // false
必须放弃雷同的响应式对象的援用,否则 Vue 不能跟踪对象的属性。当你尝试解构响应式对象的属性到本地变量时会产生如下问题:
const state = reactive({ count: 0,})// ⚠️ count 与 state 失去链接,成为本地变量let { count } = statecount += 1 // ⚠️ 不会影响原始状态
侥幸的是,你能够应用 toRefs
先将对象属性转换到 refs,而后就能够在放弃响应性的前提下将数据解构:
let state = reactive({ count: 0,})// count 是一个 ref,保留了响应性const { count } = toRefs(state)
一个类似的问题产生在你尝试重新分配一个 reactive
值。如果你尝试“替换”一个响应式对象,新的对象会笼罩初试对象的援用,从而导致对初试援用的响应性连贯失落:
const state = reactive({ count: 0,})watch(state, () => console.log(state), { deep: true })// "{ count: 0 }"// ⚠️ 下面的 {{ count: 0 }} 不再被跟踪(失落了响应性)state = reactive({ count: 10,})// watcher 不会被触发
如果咱们将其中的属性传递给了函数,同样会使响应式连贯失落:
const state = reactive({ count: 0,})const useFoo = (count) => { // ⚠️ 这里的 count 是一个一般的 number // 不能跟踪 state.count 的扭转}useFoo(state.count)
ref()
reative()
的种种限度归根结底是因为 JavaScript 没有能够作用于所有值类型的“援用”机制。为此,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) // 0count.value++console.log(count.value) // 1state.value.count = 1console.log(state.value) // { count: 1 }
你可能会有疑难,因为咱们刚刚理解了 Vue 须要一个对象来触发 get/set 代理进行跟踪,所以 ref()
如何解决根本类型的值?上面的伪代码解释了 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({}))
如果想深刻理解,可参阅 Vue 中 ref()
局部的源码: ref.ts
可怜的是,同样不能解构由 ref()
创立的对象,因为也会造成响应性失落:
import { ref } from 'vue'const count = ref(0)const countValue = count.value // ⚠️ 失落了响应性const { value: countDestructured } = count // ⚠️ 失落了响应性
然而如果 refs 从个别对象上被解构时,不会失落响应性:
const state = { count: ref(1), name: ref('Michael'),}const { count, name } = state // 依然具备响应性
Refs 同样能够在不失落响应性的前提下以参数的模式传递给函数:
const state = { count: ref(1), name: ref('Michael'),}const useFoo = (count) => { /** * 办法接管一个 ref * 须要通过 .value 拿到值 * 然而这个值依然具备响应性 */}useFoo(state.count)
这个性能很重要,因为它常常用于将逻辑提取到 组合函数 中。
一个蕴含对象类型值的 ref 能够响应式地替换整个对象:
const state = { count: 1, name: 'Michael',}// 依然具备响应性state.value = { count: 2, name: 'Chris',}
解包 refs()
在应用 refs 时,到处应用 .value
可能会很麻烦,但咱们能够应用一些辅助性的办法。
unref 办法
unref() 是一个不便的办法,如果一个变量的值是 ref
就能够施展出它的作用。在一个非 ref 的值中调用 .value
可能会抛出一个 runtime 谬误,这时 unref()
就派上了用场:
import { ref, unref } from 'vue'const count = ref(0)const unwrappedCount = unref(count)// 相似于 isRef(count) ? count.value: count
如果参数是 ref
则 undef()
返回其外部的值,否则就返回其自身。这是 val = isRef(val) ? val.value : val
计算的一个语法糖。
在模板中的解包
当 ref 在模板中调用时,Vue 会应用 unref() 主动“解包”,所以不须要在模板中应用 .value
:
<script setup>import { ref } from 'vue'const count = ref(0)</script><template> <span> <!-- 不须要应用 .value --> {{ count }} </span></template>
留神:仅在 ref 在模板中作为顶层属性被拜访时起作用。
Watcher
咱们能够间接将 ref
作为 watcher 依赖传递:
import { watch, ref } from 'vue'const count = ref(0)// Vue 会主动解包 refwatch(count, (newCount) => console.log(newCount))
Volar
如果你应用 VS Code,你能够应用 Volar 插件主动给 refs 增加 .value
。能够在设置中的 Volar: Auto Complete Refs
选项开启。
也能够在 JSON 设置中开启:
"volar.autoCompleteRefs": true
留神:为了缩小 CPU 的占用率,这个性能默认是敞开的。
reative() 和 ref() 的比照
让咱们来总结一下这两个 API 的不同:
reactive | ref |
---|---|
只对 object 类型无效 | 对任意类型无效 |
在 <script> 和 <template> 中无差别应用 | 在 <script> 和 <template> 应用形式不同 |
重新分配一个新对象会失落响应性 | 可重新分配 object 援用 |
可不通过 .value 拜访属性 | 须要应用 .value 拜访属性 |
可将援用传递给函数 | |
解构时会失落响应性 | |
相似于 Vue2 的数据对象 |
我的观点
我比拟喜爱 ref
,如果你看到通过 .value
拜访其属性值,则晓得它是一个响应式的值。如果是应用 reactive
创立的对象,则不是那么分明晓得这是一个响应式对象:
anyObject.property = 'new' // anyObject 可能被当作一个一般的 JS 对象或者一个响应式的对象anyRef.value = 'new' // 看起来是一个 ref
这个假如是对的,如果你对响应式根底有理解的话,就会晓得应该应用 .value
拜访响应式的值。
如果你应用 ref
,你应该防止应用一个无响应式的 key 值为 value
的属性:
const dataFromApi = { value: 'abc', name: 'Test' }const reactiveData = ref(dataFromApi)const valueFromApi = reactiveData.value.value //
如果你刚开始应用组合式 API,须要将我的项目从选项式 API 适度到组合式 API, reactive
可能更直观和不便实现迁徙。reactive
与 data
字段内的响应式属性十分类似:
// OptionsApiComponent.vue<script>export default { data() { count: 0, name: 'MyCounter' }, methods: { increment() { this.count += 1; }, }};</script>
你只须要简略地将 data
中的任何数据复制到 reactive
中,就能够实现迁徙到组合式 API:
// CompositionApiComponent.vue<script setup>setup() { // 与选项式 API 中的 'data' 相等 const state = reactive({ count: 0, name: 'MyCounter' }); const {count, name} = toRefs(statee) // 与选项式 API 中的 'methods' 相等 increment(username) { state.count += 1; }}</script>
组合应用 ref 和 reactive
一种举荐应用的模式是在 reactive
对象中嵌套 refs:
const loading = ref(true)const error = ref(null)const state = reactive({ loading, error,})// 能够 watch 响应式 objectwatchEffect(() => console.log(state.loading))// ...和 refwatch(loading, () => console.log('loading has changed'))setTimeout(() => { loading.value = false // 触发所有的 watchers}, 500)
如果你不须要响应式的 state
object,则能够将 refs 放入一般的 JavaScript object 中。
将 refs 放入一个组中便于解决同时放弃代码的直观整洁,能够间接看到组内的 refs 相关联性。
Vue 社区的观点
Michael Thiessen 写了一篇对于这个话题的精彩深刻的文章,收集了 Vue 社区中大佬们的观点:Ref vs. Reactive — Which is Best?
总的来说,他们默认都应用 ref
,当须要分组时应用 reactive
。
论断
所以,你应该应用 ref
还是 reactive
?
我举荐默认应用 ref
,当须要分组的时候应用 reactive
。和 Vue 社区的观点一样,然而如果你决定默认应用 reactive
,也是齐全能够的。
ref
和 reactive
都是 Vue3 中十分有用的创立响应式变量的办法。你甚至能够在没有任何技术缺点的状况下应用两者。只须要抉择一个你最喜爱的形式,尝试在你的代码格调中保持一致!
如果你喜爱这篇文章,能够关注作者 @Mokkapps 获取更多内容,同时能够及时接管新的文章推送。