知道大家使用Vue3 的時候有沒有這樣的疑惑,“ref、rective 都能創建一個響應式對象,我該如何選擇?”,“為什麼響應式對象解構之後就失去了響應式?應該如何處理?」 今天咱們就來全面盤點一下ref、reactive,相信看完你一定會有不一樣的收穫,一起學起來吧!
在Vue3 中我們可以使用reactive()
建立一個響應式物件或陣列:
import { reactive } from 'vue' const state = reactive({ count: 0 })
這個響應式物件其實就是一個Proxy
, Vue 會在這個Proxy
的屬性被存取時收集副作用,屬性被修改時觸發副作用。
要在元件範本中使用響應式狀態,需要在 setup()
函數中定義並傳回。 【相關推薦:vuejs影片教學、web前端開發】
<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 }){{ state.count }}
#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
在Vue 中,狀態預設都是深層響應式的。但在某些場景下,我們可能想要建立一個 淺層響應式物件
,讓它只在頂層具有響應性,這時候可以使用 shallowReactive()
。
const state = shallowReactive({ foo: 1, nested: { bar: 2 } }) // 状态自身的属性是响应式的 state.foo++ // 下层嵌套对象不是响应式的,不会按期望工作 state.nested.bar++
注意:淺層響應式物件應該只用於元件中的根級狀態。避免將其嵌套在深層的響應式物件中,因為其內部的屬性具有不一致的回應行為,嵌套之後將很難理解和調試。
reactive() 雖然強大,但也有以下幾個限制:
僅對物件類型有效(物件、陣列和Map
、Set
這樣的集合類型),而對string
、number
和boolean
這樣的原始型別無效。
因為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>
將響應式物件的屬性賦值或解構至本地變量,或將該屬性傳入一個函數時,會失去響應性:
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
閃耀登場了!
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 对象上 value
属性的值。常用的两种方法就是 .value
和 unref()
。 unref()
是 Vue 提供的方法,如果参数是 ref ,则返回 value 属性的值,否则返回参数本身。
当 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>
现在结果就可以正确地渲染出来了。
当一个 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
跟响应式对象不同,当 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
是基于响应式对象上的一个属性,创建一个对应的 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()
是将一个响应式对象上的所有属性都转为 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 中的响应式是采⽤ Object.defineProperty() , 通过 getter / setter 进行属性的拦截。这种方式对旧版本浏览器的支持更加友好,但它有众多缺点:
初始化时只会对已存在的对象属性进行响应式处理。也是说新增或删除属性,Vue 是监听不到的。必须使用特殊的 API 处理。
数组是通过覆盖原型对象上的7个⽅法进行实现。如果通过下标去修改数据,Vue 同样是无法感知的。也要使用特殊的 API 处理。
无法处理像 Map
、 Set
这样的集合类型。
带有响应式状态的逻辑不方便复用。
针对上述情况,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 的响应式系统又提升了一个台阶!
以上是全面盤點vue3中的ref、reactive的詳細內容。更多資訊請關注PHP中文網其他相關文章!