ref
與 reactive
都屬於深層響應的 API 。它們會遞迴地將內部所有巢狀物件都轉換為響應式代理 。多數情況下非常方便,但當處理大型資料結構時,這種深度監聽的效能開銷可能會造成瓶頸。這時候我們就會使用到 shallowRef
或 shallowReactive
:
ShallowRef
shallowRef
會建立一個 ref,但只對 .value
屬性本身的賦值操作具有響應性,它並不會將 .value
的內容轉換為響應式物件。
1<body>2 <div id="app"></div>3 <script type="module">4 import { shallowReactive, shallowRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'5 // import { reactive, effect, toRefs } from '../dist/reactivity.esm.js'6
7 const c = shallowRef({8 count: 19 });10
11 effect(() => {12 console.log(c.value.count)13 })14
15 setTimeout(() => {4 collapsed lines
16 c.value.count++17 }, 1000)18 </script>19</body>
查看上述程式碼,你會觀察到effect
只輸出一次,然而一秒鐘後setTimeout
,並沒有輸出第二次。
如果希望輸出第二次的話,必須要把c.value.count++
更改為c.value = { count: 2 }
。
依照慣例我們可以看到,console.log(c)
的結果,他是屬於一個 RefImpl
類別的物件,但差異性是__v_isShallow
是true
,由此我們可以知道它們是一樣的東西,但是透過這個標記來告訴類別說,我們現在這個物件是不是屬於淺層響應式物件。
1// 如果 value 是物件,則使用 reactive 轉換為響應式物件2const convert = (value) => isObject(value) ? reactive(value) : value3
4class RefImpl implements Dependency{5 _value;6 [ReactiveFlags.IS_REF] = true7 private _isShallow: boolean8
9 subs: Link10 subsTail: Link11 constructor(value, isShallow) {12 // 如果 isShallow 是 true,代表屬於淺層物件,直接回傳13 this._value = isShallow ? value : convert(value)14 }15
16 collapsed lines
16 get value() {17 if (activeSub) {18 trackRef(this)19 }20 return this._value21 }22
23 set value(newValue) {24 // 如果新值和舊值發生過變化,則更新25 if(hasChanged(newValue, this._value)){26 // 如果 isShallow 是 true,代表屬於淺層物件,直接回傳27 this._value = this._isShallow ? newValue : convert(newValue)28 triggerRef(this)29 }30 }31}
所以我們的實現思路是這樣:
- 幫
RefImpl
類別增加一個private
的_isShallow
屬性 - 將「檢查值是否為物件,若是則用
reactive
轉換」的邏輯封裝成convert
函式 。 - 在
constructor
和setter
中,根據_isShallow
標記來決定是直接賦值,還是要經過convert
函式處理 。
ShallowReactive
shallowReactive
只對物件的第一層屬性進行代理,任何對巢狀物件的修改都不會觸發響應 。
1<body>2 <div id="app"></div>3 <script type="module">4 import { shallowReactive, shallowRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'5 // import { reactive, effect, toRefs } from '../dist/reactivity.esm.js'6
7 const state = shallowReactive({8 a:1,9 b:{10 c:111 }12 })13
14 effect(() => {15 console.log(state.b.c)7 collapsed lines
16 })17
18 setTimeout(() => {19 state.b.c++20 }, 1000)21 </script>22</body>
shallowReactive
也是一樣的效果,state.b.c++
沒有反應,需要變更為state.b = { c: 2 } }
,effect
才會觸發輸出。
reactive
是通過mutableHandlers
來處理,因此我們需要先重構這個mutableHandlers
:
1const set = (target, key, newValue, receiver) => {2 ...3 ...4}5
6const get = (target, key, receiver) => {7 // 收集依賴:綁定target的屬性與effect的關係8 track(target, key)9 const res = Reflect.get(target, key,receiver)10 // 如果 res 是一個 ref,則返回 res.value11 if(isRef(res)){12 // target = {a:ref(0)}13 return res.value14 }14 collapsed lines
15
16 if(isObject(res)){17 /**18 * 如果 res 是物件,則將其轉換為響應式物件19 */20 return reactive(res)21 }22 return res23}24
25export const mutableHandlers = {26 get,27 set28}
先將 getter
跟 setter
抽出來,再來調整 getter
。
1function createGetter(isShallow = false) {2 return function get(target, key, receiver) {3 track(target, key)4 const res = Reflect.get(target, key, receiver)5 if (isRef(res)) {6 return res.value7 }8
9 if (isObject(res)) {10 // 關鍵:如果是 shallow,直接回傳原始物件,不再遞迴呼叫 reactive11 return isShallow ? res : reactive(res)12 }13 return res14 }15}2 collapsed lines
16
17const get = createGetter()
我們將get
的邏輯抽離成一個工廠函式 createGetter
,並用 isShallow
參數來控制是否要遞迴地將巢狀物件轉換為 reactive
。
1const shallowGet = createGetter(true)2
3export const shallowReactiveHandlers = {4 get: shallowGet,5 set6}
建立不同 handler 並導出,之後我們在reactive.ts
引入
1import { mutableHandlers, shallowReactiveHandlers } from './baseHandlers'2
3function createReactiveObject(target, handlers, proxyMap) {4
5 if (!isObject(target)) return target6
7 // 如果這個 target 儲存在 reactiveSet 中8 // 表示 target 是一個響應式物件,直接返回已經建立好的 proxy9 if(reactiveSet.has(target)){10 return proxyMap.get(target)11 }12
13 // 如果這個 target 已經被 reactive 過了,直接返回已經建立好的 proxy14 const existingProxy = proxyMap.get(target)14 collapsed lines
15 if (existingProxy) {16 return existingProxy17 }18
19 // 根據傳入參數 handler,建立 target 的代理物件20 const proxy = new Proxy(target, handlers)21
22 // 儲存 target 和響應式物件的關聯關係23 proxyMap.set(target, proxy)24
25 reactiveSet.add(proxy)26
27 return proxy28}
調整createReactiveObject
函式,讓它可以根據 handler
以及 proxyMap
來決定是不是要建立深層響應式物件。
1// 新增 shallowReactive 的快取2const shallowReactiveMap = new WeakMap()3
4export function reactive(target) {5 return createReactiveObject(target, mutableHandlers, reactiveMap)6}7
8// 新增 shallowReactive 函式9export function shallowReactive(target) {10 return createReactiveObject(target, shallowReactiveHandlers, shallowReactiveMap)11}
這樣我們就完成了 shallowReactive
,現在shallowReactive
以及shallowRef
都已經完成。
「深層響應」為帶來了開發上的便利,而「淺層響應」則讓我們有更近一步優化效能的機會。在一般開發過程中,我們應該優先使用 ref
和 reactive
,當遇到明確的效能瓶頸時,可以觀察我們自己在使用資料的情況,適度替換成shallow
版本。