在本系列的上一篇文章前端
帶你完全搞懂Vue3的響應式原理!TypeScript從零實現基於Proxy的響應式庫。中react
咱們詳細的講解了普通對象和數組實現響應式的原理,可是Proxy能夠作的遠不止於此,對於es6中新增的Map
、Set
、WeakMap
、WeakSet
也同樣能夠實現響應式的支持。git
可是對於這部分的劫持,代碼中的邏輯是徹底獨立的一套,這篇文章就來看一下如何基於函數劫持實現實現這個需求。es6
閱讀本篇須要的一些前置知識:
Proxy
WeakMap
Reflect
Symbol.iterator (會講解)github
在上一篇文章中,假設咱們經過data.a
去讀取響應式數據data
的屬性,則會觸發Proxy的劫持中的get(target, key)
api
target就是data對應的原始對象
,key就是a
數組
咱們能夠在這時候給key: a
註冊依賴,而後經過Reflect.get(data, key)去讀到原始數據返回出去。markdown
回顧一下:數據結構
/** 劫持get訪問 收集依賴 */ function get(target: Raw, key: Key, receiver: ReactiveProxy) { const result = Reflect.get(target, key, receiver) // 收集依賴 registerRunningReaction({ target, key, receiver, type: "get" }) return result } 複製代碼
而當咱們的響應式對象是一個Map
數據類型的時候,想象一下這個場景:app
const data = reactive(new Map([['a', 1]])) observe(() => data.get('a')) data.set('a', 2) 複製代碼
讀取數據的方式變成了data.get('a')
這種形式,若是仍是用上一篇文章中的get,會發生什麼狀況呢?
get(target, key)
中的target是map原始對象
,key是get
,
經過Reflect.get返回的是map.get
這個function
,這時候就經過get
這個key註冊依賴,這並非咱們想要的,咱們想要的效果是經過a
這個key來註冊依賴。
因此這裏的辦法就是函數劫持
,想象一下咱們把對map上全部key的訪問所有劫持掉,好比用戶去使用map.get
,這個get若是訪問的是咱們本身實現的get函數,那麼這個get函數裏就能夠自由的作任何事情,好比收集依賴
~
那麼接下里的目標就是把對於Map
和Set
的全部api的訪問(好比has
, get
, set
, add
)所有替換成咱們本身寫的方法,讓用戶無感知的使用這些api,可是內部卻已經被咱們本身的代碼劫持了。
咱們把上篇文章中的目錄結構調整成這樣:
src/handlers // 數組和對象的handlers ├── base.ts // map和set的handlers ├── collections.ts // 統一導出 └── index.ts 複製代碼
首先看一下handlers/index.ts入口的改造
import { collectionHandlers } from "./collections" import { baseHandlers } from "./base" import { Raw } from "types" // @ts-ignore // 根據對象的類型 獲取Proxy的handlers export const handlers = new Map([ [Map, collectionHandlers], [Set, collectionHandlers], [WeakMap, collectionHandlers], [WeakSet, collectionHandlers], [Object, baseHandlers], [Array, baseHandlers], [Int8Array, baseHandlers], [Uint8Array, baseHandlers], [Uint8ClampedArray, baseHandlers], [Int16Array, baseHandlers], [Uint16Array, baseHandlers], [Int32Array, baseHandlers], [Uint32Array, baseHandlers], [Float32Array, baseHandlers], [Float64Array, baseHandlers], ]) /** 獲取Proxy的handlers */ export function getHandlers(obj: Raw) { return handlers.get(obj.constructor) } 複製代碼
這裏定義了一個Map: handlers
,導出了一個getHandlers
方法,根據傳入數據的類型獲取Proxy的第二個參數handlers
,
baseHandlers
在第一篇中已經進行了詳細講解。
這篇文章主要是講解collectionHandlers
。
先看一下collections
的入口:
// 真正交給Proxy第二個參數的handlers只有一個get // 把用戶對於map的get、set這些api的訪問所有移交給上面的劫持函數 export const collectionHandlers = { get(target: Raw, key: Key, receiver: ReactiveProxy) { // 返回上面被劫持的api target = hasOwnProperty.call(instrumentations, key) ? instrumentations : target return Reflect.get(target, key, receiver) }, } 複製代碼
咱們全部的handlers只有一個get
,也就是用戶對於map或者set上全部api的訪問(好比has
, get
, set
, add
),都會被轉移到咱們本身定義的api上,這其實就是函數劫持的一種應用。
那關鍵就在於instrumentations
這個對象上,咱們對於這些api的本身的實現。
export const instrumentations = { get(key: Key) { // 獲取原始數據 const target = proxyToRaw.get(this) // 獲取原始數據的__proto__ 拿到原型鏈上的方法 const proto: any = Reflect.getPrototypeOf(this) // 註冊get類型的依賴 registerRunningReaction({ target, key, type: "get" }) // 調用原型鏈上的get方法求值 而後對於複雜類型繼續定義成響應式 return findReactive(proto.get.apply(target, arguments)) }, set(key: Key, value: any) { const target = proxyToRaw.get(this) const proto: any = Reflect.getPrototypeOf(this) // 是不是新增的key const hadKey = proto.has.call(target, key) // 拿到舊值 const oldValue = proto.get.call(target, key) // 求出結果 const result = proto.set.apply(target, arguments) if (!hadKey) { // 新增key值時以type: add觸發觀察函數 queueReactionsForOperation({ target, key, value, type: "add" }) } else if (value !== oldValue) { // 已存在的key的值發生變化時以type: set觸發觀察函數 queueReactionsForOperation({ target, key, value, oldValue, type: "set" }) } return result }, } /** 對於返回值 若是是複雜類型 再進一步的定義爲響應式 */ function findReactive(obj: Raw) { const reactiveObj = rawToProxy.get(obj) // 只有正在運行觀察函數的時候纔去定義響應式 if (hasRunningReaction() && isObject(obj)) { if (reactiveObj) { return reactiveObj } return reactive(obj) } return reactiveObj || obj } 複製代碼
核心的get
和set
方法和上一篇文章中的實現就幾乎同樣了,get
返回的值經過findReactive
確保進一步定義響應式數據,從而實現深度響應。
至此,這樣的用例就能夠跑通了:
const data = reactive(new Map([['a', 1]])) observe(() => console.log('a', data.get('a'))) data.set('a', 5) // 從新打印出a 5 複製代碼
接下來再針對一些特有的api進行實現:
has (key) { const target = proxyToRaw.get(this) const proto = Reflect.getPrototypeOf(this) registerRunningReactionForOperation({ target, key, type: 'has' }) return proto.has.apply(target, arguments) }, 複製代碼
add就是典型的新增key的流程,會觸發循環相關的觀察函數。
add (key: Key) { const target = proxyToRaw.get(this) const proto: any = Reflect.getPrototypeOf(this) const hadKey = proto.has.call(target, key) const result = proto.add.apply(target, arguments) if (!hadKey) { queueReactionsForOperation({ target, key, value: key, type: 'add' }) } return result }, 複製代碼
delete也和上一篇中的deleteProperty的實現大體相同,會觸發循環相關的觀察函數。
delete (key: Key) { const target = proxyToRaw.get(this) const proto: any = Reflect.getPrototypeOf(this) const hadKey = proto.has.call(target, key) const result = proto.delete.apply(target, arguments) if (hadKey) { queueReactionsForOperation({ target, key, type: 'delete' }) } return result }, 複製代碼
clear () { const target: any = proxyToRaw.get(this) const proto: any = Reflect.getPrototypeOf(this) const hadItems = target.size !== 0 const result = proto.clear.apply(target, arguments) if (hadItems) { queueReactionsForOperation({ target, type: 'clear' }) } return result }, 複製代碼
在觸發觀察函數的時候,針對clear這個type作了一些特殊處理,也是觸發循環相關的觀察函數。
export function getReactionsForOperation ({ target, key, type }) { const reactionsForTarget = connectionStore.get(target) const reactionsForKey = new Set() + if (type === 'clear') { + reactionsForTarget.forEach((_, key) => { + addReactionsForKey(reactionsForKey, reactionsForTarget, key) + }) } else { addReactionsForKey(reactionsForKey, reactionsForTarget, key) } if ( type === 'add' || type === 'delete' + || type === 'clear' ) { const iterationKey = Array.isArray(target) ? 'length' : ITERATION_KEY addReactionsForKey(reactionsForKey, reactionsForTarget, iterationKey) } return reactionsForKey } 複製代碼
clear
的時候,把每個key收集到的觀察函數都給拿到,而且把循環的觀察函數也拿到,能夠說是觸發最全的了。
邏輯也很容易理解,clear
的行爲每個key都須要關心,只要在observe函數中讀取了任意的key,clear的時候也須要從新執行這個observe的函數。
forEach (cb, ...args) { const target = proxyToRaw.get(this) const proto = Reflect.getPrototypeOf(this) registerRunningReaction({ target, type: 'iterate' }) const wrappedCb = (value, ...rest) => cb(findObservable(value), ...rest) return proto.forEach.call(target, wrappedCb, ...args) }, 複製代碼
到了forEach的劫持 就稍微有點難度了。
首先registerRunningReaction
註冊依賴的時候,用的key是iterate
,這個很容易理解,由於這是遍歷的操做。
這樣用戶後續對集合數據進行新增
或者刪除
、或者使用clear
操做的時候,會從新觸發內部調用了forEach
的觀察函數
重點看下接下來這兩段代碼:
const wrappedCb = (value, ...rest) => cb(findObservable(value), ...rest) return proto.forEach.call(target, wrappedCb, ...args) 複製代碼
wrappedCb包裹了用戶本身傳給forEach的cb函數,而後傳給了集合對象原型鏈上的forEach,這又是一個函數劫持。用戶傳入的是map.forEach(cb),而咱們最終調用的是map.forEach(wrappedCb)。
在這個wrappedCb中,咱們把cb中本應該得到的原始值value經過findObservable
定義成響應式數據交給用戶,這樣用戶在forEach中進行的響應式操做同樣能夠收集到依賴了,不得不讚嘆這個設計的巧妙。
get size () { const target = proxyToRaw.get(this) const proto = Reflect.getPrototypeOf(this) registerRunningReaction({ target, type: 'iterate' }) return Reflect.get(proto, 'size', target) }, keys () { const target = proxyToRaw.get(this) const proto: any = Reflect.getPrototypeOf(this) registerRunningReaction({ target, type: 'iterate' }) return proto.keys.apply(target, arguments) }, 複製代碼
因爲keys
和size
返回的值不須要定義成響應式,因此直接返回原值就能夠了。
再來看一個須要作特殊處理的典型
values () { const target = proxyToRaw.get(this) const proto: any = Reflect.getPrototypeOf(this) registerRunningReaction({ target, type: 'iterate' }) const iterator = proto.values.apply(target, arguments) return patchIterator(iterator, false) }, 複製代碼
這裏有一個知識點須要注意一下,就是集合對象的values方法返回的是一個迭代器對象Map.values,
這個迭代器對象每一次調用next()
都會返回Map中的下一個值
,爲了讓next()獲得的值也能夠變成響應式proxy
,咱們須要用patchIterator
劫持iterator
// 把iterator劫持成響應式的iterator function patchIterator (iterator) { const originalNext = iterator.next iterator.next = () => { let { done, value } = originalNext.call(iterator) if (!done) { value = findReactive(value) } return { done, value } } return iterator } 複製代碼
也是經典的函數劫持邏輯,把原有的{ done, value }
值拿到,把value值定義成響應式proxy
。
理解了這個概念之後,剩下相關幾個handler也好理解了
entries () { const target = proxyToRaw.get(this) const proto: any = Reflect.getPrototypeOf(this) registerRunningReaction({ target, type: 'iterate' }) const iterator = proto.entries.apply(target, arguments) return patchIterator(iterator, true) }, 複製代碼
對應entries
也有特殊處理,把迭代器傳給patchIterator
的時候須要特殊標記一下這是entries
,看一下patchIterator
的改動:
/** 把iterator劫持成響應式的iterator */ function patchIterator (iterator, isEntries) { const originalNext = iterator.next iterator.next = () => { let { done, value } = originalNext.call(iterator) if (!done) { + if (isEntries) { + value[1] = findReactive(value[1]) } else { value = findReactive(value) } } return { done, value } } return iterator } 複製代碼
entries操做的每一項是一個[key, val]的數組,因此經過下標[1],只把值定義成響應式,key不須要特殊處理。
[Symbol.iterator] () { const target = proxyToRaw.get(this) const proto: any = Reflect.getPrototypeOf(this) registerRunningReaction({ target, type: 'iterate' }) const iterator = proto[Symbol.iterator].apply(target, arguments) return patchIterator(iterator, target instanceof Map) }, 複製代碼
這裏又是一個比較特殊的處理了,[Symbol.iterator]
這個內置對象會在for of
操做的時候被觸發,具體能夠看本文開頭給出的mdn文檔。因此也要用上面的迭代器劫持的思路。
patchIterator的第二個參數,是由於對Map
數據結構使用for of
操做的時候,返回的是entries結構,因此也須要進行特殊處理。
既然本篇講到了Map,我想到了在TS中對Map作類型推斷是不友好的,好比以下的方法:
function createMap<T extends object, K extends keyof T>(obj: T) { const map = new Map<K, T>() Object.keys(obj).forEach((key) => { map.set(key as K, obj[key]) }) return map } // 提示出來的類型是 { // a: number; // b: string; // } const a = createMap({a: 1, b: '2'}).get('a') 複製代碼
因爲Map是調用set去賦值的,ts沒有辦法很好的去進行類型推斷,把key值對應的類型給精準的推斷出來,若是咱們用本文的劫持
思路呢?
本文的代碼都在這個倉庫裏
github.com/sl1673495/p…
函數劫持的思路在各類各樣的前端庫中都有出現,這幾乎是進階必學的一種技巧了,但願經過本文的學習,你能夠理解函數劫持的一些強大的做用。也能夠想象Vue3裏用proxy來實現響應式能力有多麼強。