10.07 Vue3.0採用新特性Proxy來實現數據狀態的響應,它的原理是什麼?

from-> yck:https://juejin.im/post/5d996e3e6fb9a04e3043cc5b

Vue3.0採用新特性Proxy來實現數據狀態的響應,它的原理是什麼?

Vue3 使用了 Proxy 替換了原來的 Object.defineProperty 來實現數據響應。

reactive()使用

很簡單,直接Vue引入reactive方法,接收一個對象參數,就實現了數據的響應式:

const state = reactive({ num: 0 })
effect(() => {
console.log(state.num)
})
state.num = 100

reactive 內部的核心代碼簡化如下:

Vue3.0採用新特性Proxy來實現數據狀態的響應,它的原理是什麼?

首先判斷傳入的參數類型是否可以用於觀察,目前支持的類型為 Object|Array|Map|Set|WeakMap|WeakSet。

接下來判斷參數的構造函數,根據類型獲得不同的 handlers。這裡我們就統一使用 baseHandlers,因為這個已經覆蓋 99% 的情況了。只有 Set, Map, WeakMap, WeakSet 才會使用到 collectionHandlers。

對於 baseHandlers 來說,最主要的是劫持了 get 和 set 行為,這兩個行為同時也能原生劫持數組下標修改值及對象新增屬性的行為,這一點非常重要,因為Object.defineProperty就不行。

最後就是構造一個 Proxy 對象完成數據的響應式。相比 Object.defineproperty 一開始就要遞歸遍歷整個對象的做法來說,使用 Proxy 性能會好得多。比如原來 forEach 遍歷:

Object.keys(value).forEach((key) => {
defineReactive(value, key, value[key]);
});

接下來當我們去使用 state 對象的時候,就能劫持到內部的行為。

讀取時:state.num 就會觸發 get 函數;

修改時:state.num = 100 就會觸發 set 函數。

以下是這兩個函數的核心(TS語法):

function get(target: any, key: string | symbol, receiver: any) {
// 獲得結果
const res = Reflect.get(target, key, receiver)
track(target, OperationTypes.GET, key)
// 判斷是否為對象,是的話將對象包裝成 proxy
return isObject(res) ? reactive(res) : res
}

對於 get 函數來說,獲取值肯定是最核心的一步驟了。接下來是調用 track,這個和 effect 有關,下文再說。最後是判斷值的類型,如果是對象的話就繼續包裝成 Proxy。

function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
const result = Reflect.set(target, key, value, receiver)
if (是否新增 key) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key)
}
return result
}

對於 set 函數來說,設置值是第一步驟,然後調用 trigger,這也是 effect 中的內容。

簡單來說,如果某個 effect 回調中有使用到 state.num,那麼這個回調會被收集起來,並在調用 state.num =100 時觸發。

那麼怎麼收集這些內容呢?這就要說說 targetMap 這個對象了。它用於存儲依賴關係,類似以下結構,這個結構會在 effect 文件中被用到

{
target: {
key: Dep
}
}

先來解釋下三者到底是什麼,這個很重要

  • target 就是被 proxy 的對象
  • key 是對象觸發 get 行為以後的屬性。比如 counter.num 觸發了 get 行為,num 就是 key
  • dep 是回調函數,也就是 effect 中調用了 counter.num 的話,這個回調就是 dep,需要收集起來下次使用

這裡筆者把這些內容脫離源碼串起來講一下流程。

const counter = reactive({ num: 0 })
effect(() => {
console.log(counter.num)
})
counter.num = 7

首先創建一個 Proxy 對象,targetMap 會把這個對象收集起來當做 key。

接下來調用 effect 回調的時候會把這個回調保存起來,用於下面的依賴收集。在調用的過程中會觸發 counter 的 get 函數,內部調用了 track 函數,這個函數會使用到 targetMap。

這裡首先通過 target 從 targetMap 中取到一個對象,這個對象也就是 target 所有的依賴關係。那麼對於 counter.num 來說,num 就是這個對象的 key(這裡如果有點模糊的話可以先看下上面的數據結構),值是一個依賴回調的集合,因為 counter.num 可能會被多個地方依賴到。

回調執行完畢以後會把保存的回調銷燬掉。

當我們調用 counter.num = 7 時,觸發 set 函數,內部調用 trigger 函數,同樣會使用到 targetMap。

同樣通過 target 取到一個對象,然後通過 key 也就是 num 去取出依賴集合,最後遍歷這個集合執行裡面所有的回調函數。

另外對於 computed 來說,內部也是使用到了 effect,無非它的回調不會在調用 effect 後立即執行,只有當觸發 get 行為以後才會執行回調並進行依賴收集,舉個例子:

const state= reactive({ num: 0 }) 

const cValue = computed(() => state.num)
state.num = 1

對於以上代碼來說,computed 的回調永遠不會執行,只有當使用到了 cValue.value 時才會執行回調,然後接下來的操作就和上面的沒區別了。


分享到:


相關文章: