在本系列的上一篇文章前端
帶你完全搞懂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)去讀到原始數據返回出去。數據結構
回顧一下:app
/** 劫持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
數據類型的時候,想象一下這個場景:函數
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
這個方法,註冊的依賴也是經過get
這個key註冊的,而咱們想要的效果是經過a
這個key來註冊依賴。
因此這裏的辦法就是函數劫持
,就是把對於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結構,因此也須要進行特殊處理。
本文的代碼都在這個倉庫裏
github.com/sl1673495/p…
函數劫持的思路在各類各樣的前端庫中都有出現,這幾乎是進階必學的一種技巧了,但願經過本文的學習,你能夠理解函數劫持的一些強大的做用。也能夠想象Vue3裏用proxy來實現響應式能力有多麼強。