本篇我們會繼續探索reactive
函數中對Map/WeakMap/Set/WeakSet
對象的代理實現。
Map/WeakMap/Set/WeakSet的操作
由於WeakMap和WeakSet分別是Map和Set的不影響GC執行垃圾回收的版本,這裏我們只研究Map和Set即可。
Set的屬性和方法
size: number
爲訪問器屬性(accessor property),返回Set對象中的值的個數add(value: any): Set
向Set對象隊尾添加一個元素clear(): void
移除Set對象內所有元素delete(value: any): boolean
移除Set中與入參值相同的元素,移除成功則返回truehas(value: any): boolean
判斷Set中是否存在與入參值相同的元素values(): Iterator
返回一個新的迭代器對象,包含Set對象中按插入順序排列的所有元素keys(): Iterator
和values(): Iterator
一樣的功效@@iterator
和values(): Iterator
一樣的功效,for of
中調用entries(): Iterator
返回一個新的迭代器對象,包含Set對象中按插入順序排列的所有元素,但爲與Map使用一致每次迭代返回的內容爲[value, value]
forEach(callbackFn: { (value: any, set: Set) => any } [, thisArg])
按插入順序遍歷Set對象的每一個元素
Map的屬性和方法
size: number
爲訪問器屬性(accessor property),返回Set對象中的值的個數set(key: any, value: any): Map
向Map對象添加或更新一個指定鍵的值clear(): void
移除Map對象內所有鍵值對delete(key: any): boolean
移除Map對象中指定的鍵值對,移除成功則返回truehas(key: any): boolean
判斷Map中是否存在鍵與入參值相同的鍵值對values(): Iterator
返回一個新的迭代器對象,包含Map對象中按插入順序排列的所有值keys(): Iterator
返回一個新的迭代器對象,包含Map對象中按插入順序排列的所有鍵@@iterator
和entries(): Iterator
一樣的功效,for of
中調用entries(): Iterator
返回一個新的迭代器對象,包含Map對象中按插入順序排列的所有鍵值對forEach(callbackFn: { (value: any, key: any, map: Map) => any } [, thisArg])
按插入順序遍歷Map對象的每一個鍵值對get(key: any): any
返回Map對象中指定鍵對應的值,若沒有則返回undefined
逐行看代碼我是認真的
// reactive.ts
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
由於Map/Set不像Object或Array那樣可直接通過屬性訪問的方式獲取其中的元素,而是通過add
,has
,delete
操作,因此需要像處理Array的slice
等方法那樣代理Map/Set的這些方法。
// collectionHandlers.ts
type MapTypes = Map<any, any> | WeakMap<any, any>
type SetTypes = Set<any, any> | WeakSet<any, any>
// 代理Map/Set原生的方法
// 沒有代理返回迭代器的方法??
const mutableInstrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key)
}
get size() {
// 原生的size屬性就是一個訪問器屬性
return size(this as unknown as IterableCollections)
},
has,
add,
set,
delete: deleteEntry, // delete 是關鍵字不能作爲變量或函數名稱
clear,
forEach: createForEach(false, false)
}
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
}
else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
else if (key === ReactiveFlags.RAW) {
return target
}
// 代理Map/WeakMap/Set/WeakSet的內置方法
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
TypeScript小課堂:as
斷言——this as unknown as IterableCollections
在TypeScript中可通過類型聲明定義變量的類型(其中包含複合類型),而類型推導則可以根據賦值語句中右側字面量推導出變量的實際類型,或通過當前變量使用的場景推導出當前實際類型(尤其是定義爲複合類型)。但有時無法通過當前使用場景執行精確的類型推導,這時開發者可以通過as
斷言告知TypeScript編譯器該變量當前使用範圍的數據類型(要相信自己一定比編譯器更瞭解自己的代碼:D)。
那麼as unknown
即表示將類型修改爲unknown
,那麼類型爲unknown
是表示什麼呢?unknown
是TypeScript3.0引入的top type(任何其他類型都是它的subtype),意在提供一種更安全的方式替代any
類型(any
類型是top type也是bottom type,使用它意味和繞過類型檢查),具有如下特點:
- 任何其它類型都可以賦值給
unknown
類型的變量 unknown
類型的變量只能賦值給any
或unknown
類型的變量- 如果不對
unknown
類型的變量執行類型收縮,則無法執行其它任何操作
// 1. 任何其它類型都可以賦值給`unknown`類型的變量
let uncertain: unknown = 'Hello'
uncertain = 12
uncertain = { hello: () => 'Hello' }
// 2.`unknown`類型的變量只能賦值給`any`或`unknown`類型的變量
let uncertain: unknown = 'Hello'
let noSure: any = uncertain
let notConfirm: unknown = uncertain
// 3. 如果不對`unknown`類型的變量執行類型收縮,則無法執行其它任何操作
let uncertain = { hello: () => 'Hello' }
uncertain.hello() // 編譯報錯
// 通過斷言as收縮類型
(uncertain as {hello: () => string}).hello()
let uncertain: unknown = 'Hello'
// 通過typeof或instanceof收縮類型
if (typeof uncertain === 'string') {
uncertain.toLowerCase()
}
那麼as unknown
後的as IterableCollections
意圖就十分明顯了,就是對變量進行類型收縮。this as unknown as IterableCollections
其實就是as IterableCollections
啦。
然後我們逐一看看代理方法的實現吧
Map
的get
方法
get
方法只有Map對象擁有,因此其中主要思路是從Map對象中獲取值,跟蹤鍵值變化後將值轉換爲響應式對象返回即可。
但由於要處理readonly(reactive(new Map()))
這一場景,添加了很多一時讓人看不懂的代碼而已。
const getProto = <T extends CollectionTypes>(v: T): any => Reflect.getProrotypeOf(v)
// 代理Map/WeakMap的get方法
function get(
target: MapTypes, // 指向this,由於Map對象已經被代理,因此this爲代理代理
key: unknown,
isReadonly = false,
isShallow = false
) {
/**
* 1. 針對readonly(reactive(new Map()))的情況,
* target獲取的是代理對象,而rawTarget的是Map對象
* 2. 針對reactive(new Map())的情況,
* target和rawTarget都是指向Map對象
*/
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
/**
* 若key爲代理對象,那麼被代理對象和代理對象的鍵都會被跟蹤,即
* const key = { value: 'foo' }
* const pKey = reactive(key),
* const kvs = reactive(new Map())
* kvs.set(pKey, 1)
*
* effect(() => {
* console.log('pKey', kvs.get(pKey))
* })
* effect(() => {
* console.log('key', kvs.get(key))
* })
*
* kvs.set(pKey, 2)
* // 回顯 pkey 2 和 key 2
* kvs.set(key, 3)
* // 回顯 key 2
*/
const rawKey = toRaw(key)
if (key !== rawKey) {
!isReadonly && track(rawTraget, TrackOpTypes.GET, key)
}
!isReadonly && track(rawTraget, TrackOpTypes.GET, rawKey)
// 獲取Map原型鏈上的has方法用於判斷獲取成員是否存在於Map對象上
const { has } = getProto(rawTarget)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
/**
* Map對象中存在則從Map對象或代理對象上獲取值並轉換爲響應式對象返回。
* 針對readonly(reactive(new Map()))爲什麼是從響應對象上獲取值,而不是直接從Map對象上獲取值呢?
* 這是爲了保持返回的值的結構,從響應式對象中獲取值是響應式對象,在經過readonly的處理則返回的值就是readonly(reactive({value: 'foo'}))。
*/
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
}
else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
}
else if (target !== rawTarget) {
/**
* 針對readonly(reactive(new Map())),即使沒有匹配的鍵值對,也要跟蹤對響應式對象某鍵的依賴信息
* const state = reactive(new Map())
* const readonlyState = readonly(state)
*
* effect(() => {
* console.log(readonlyState.get('foo'))
* })
* // 回顯 undefined
* state.set('foo', 1)
* // 回顯 1
*/
target.get(key)
}
// 啥都沒有找到就默認返回undefined,所以啥都不用寫
}
Map
和Set
的size
訪問器屬性
function size(target: IterableCollections, isReadonly = false) {
// 針對readonly(reactive(new Map())) 或 readonly(reactive(new Set()))只需獲取響應式對象即可,因此reactive對象也會對size的訪問進行相同的操作。
target = (target as any)[RectiveFlags.RAW]
// 跟蹤ITERATE_KEY即所有修改size的操作均會觸發訪問size屬性的副作用函數
!iReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
/**
* 由於size爲訪問器屬性因此若第三個參數傳遞receiver(響應式對象),而響應式對象並沒有size訪問器屬性需要訪問的屬性和方法,則會報異常``。因此需要最終將Map或Set對象作爲size訪問器屬性的this變量。
*/
return Reflect.get(target, 'size', target)
}
Map
和Set
的has
方法
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// 和get方法代理一樣,若key爲代理對象則代理對象或被代理對象作爲鍵的鍵值對發生變化都會觸發訪問has的副作用函數
if (key !== rawKey) {
!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
}
!isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
Set
的add
方法
function add(this: SetTypes, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
// 當Set對象中沒有該元素時則觸發依賴ITERATE_KEY的副作用函數,因此ADD操作會影響Set對象的長度
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
}
Map
的set
方法
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const { has, get } = getProto(target)
// 分別檢查代理和非代理版本的key是否存在於Map對象中
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target.key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
// 當Map對象中沒有該元素時則觸發依賴ITERATE_KEY的副作用函數,因此ADD操作會影響Map對象的長度
trigger(target, TriggerOpTypes.ADD, key, value)
}
else if (hasChanged(value, oldValue)) {
// 如果新舊值不同則觸發修改,依賴該鍵值對的副作用函數將被觸發
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
注意:get
和has
方法中會同時跟蹤代理和非代理版本的鍵對應的元素變化,而set
方法則只會觸發查找到的代理或非代理版本的鍵對應的元素變化。
deleteEntry
方法
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
// 分別檢查代理和非代理版本的key是否存在於Map/Set對象中
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target.key)
}
// 如果當前操作的是Map對象則獲取舊值
const oldValue = get ? get.call(target, key) : undefined
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
注意:get
和has
方法中會同時跟蹤代理和非代理版本的鍵對應的元素變化,而deleteEntry
方法則只會觸發查找到的代理或非代理版本的鍵對應的元素變化。
Map
和Set
的clear
方法
function clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = undefined
const result = target.clear()
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
Map
和Set
的forEach
方法
function createForEach(isReadonly: boolean, isShallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown
) {
const observed = this as any
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// 將key和value都轉換爲代理對象
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
由於forEach
會遍歷所有元素(Map對象則是所有鍵值對),因此跟蹤ITERATE_KEY
即Map/Set對象元素個數發生變化則觸發forEach
函數的執行。
迭代器對象相關方法
至此我們還沒對entries
,values
,keys
和@@iterator
這些返回迭代器的對象方法進行代理,而源碼中則在最後爲mutableInstrumentations
添加這些方法的代理。
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator/*就是@@iterator*/]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
})
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean
) {
return function(
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
/**
* 1. 針對readonly(reactive(new Map()))的情況,
* target獲取的是代理對象,而rawTarget的是Map或Set對象
* 2. 針對reactive(new Map())的情況,
* target和rawTarget都是指向Map或Set對象
*/
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
const isPair = method === 'entries' || (method === Symbol.iterator && targetIsMap)
/**
* 當調用的是Map對象的keys方法,副作用函數並沒有訪問值對象,即副作用函數只依賴Map對象的鍵而沒有依賴值。
* 而鍵只能增加或刪除,值可增加、刪除和修改,那麼此時當且僅當鍵增刪即size屬性發生變化時纔會觸發副作用函數的執行。
* 若依賴值,那麼修改其中一個值也會觸發副作用函數執行。
*/
const isKeyOnly = method === 'keys' && targetIsMap
const innerIterator = target[method](...args)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly &&
track(
rawTarget,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
)
return {
// 迭代器協議
next() {
const { value, done } = innerIterator.next()
return done
? { value, done }
: {
value: isPair ? [wrap(value[0], wrap(value[1]))] : wrap(value),
done
}
},
// 可迭代協議
[Symbol.iterator]() {
return this
}
}
}
}
可迭代協議(iterable protocol)
可迭代協議(iterable protocol),用於創建迭代器(iterator)。
如下內置類型都實現了可迭代協議:
- 字符串
- 數組
- Set
- Map
- arguements對象
- NodeList等DOM集合類型
下面的語言特性將會接收可迭代協議返回的迭代器
for...of
循環- 數據解構(
const [a, b] = [1, 2]
) - 擴展操作符(
const a = [1,2], b = [...a]
) Array.from()
- 創建Set
- 創建Map
Promise.all()
接受可迭代對象Promise.race()
接受可迭代對象yield*
操作符
讓對象支持可迭代協議其實很簡單,只需實現返回迭代器的[Symbol.iterator]
方法即可。JavaScript Plain Old Object默認並沒有支持可迭代協議,那麼我們可以自行實現以下:
const iterablizeKeys = (obj: {}) => {
if (!obj[Symbol.iterator]) {
obj[Symbol.iterator] = () => {
const keys = Object.keys(obj) as const
let i = 0
// 返回一個迭代器
return {
next() {
return { value: keys[i++], done: i > keys.length }
}
}
}
}
return obj
}
const iterableObj = iterablizeKeys({a: 1, b: 2})
for (let item of iterableObj) {
console.log(item)
}
// 回顯 a
// 回顯 b
Array.from(iterableObj) // 返回 ['a', 'b']
迭代器協議(iterator protocol)
迭代器協議(iterator protocol),提供不接受任何參數並返回IteratorResult
對象的next
方法,而IteratorResult
對象包含指向當前元素的value
屬性和表示迭代是否已結束的done
屬性,當done
屬性值爲true
時表示迭代已結束。
迭代器協議的實現正如上面可迭代協議的示例中那樣,不過我們還可以將可迭代協議和迭代對象在同一個對象上實現。
const iterablizeKeys = (obj: {}) => {
if (!obj[Symbol.iterator]) {
let iteratorState = {
keys: []
i: 0
}
// 迭代器協議
obj.next = () => ({ value: iteratorState.keys[iteratorState.i++], done: iteratorState.i > iteratorState.key.length })
// 可迭代協議
obj[Symbol.iterator] = () => {
iteratorState.keys = Object.keys(obj) as const
iteratorState.i = 0
// 返回一個迭代器
return this
}
}
return obj
}
const iterableObj = iterablizeKeys({a: 1, b: 2})
for (let item of iterableObj) {
console.log(item)
}
// 回顯 a
// 回顯 b
Array.from(iterableObj) // 返回 ['a', 'b']
總結
本篇我們通過逐行閱讀源碼瞭解到reactive如何處理Map和Set對象了,下一篇我們將開始以effect
爲入口進一步瞭解副作用函數是如何通過track
和trigger
記錄依賴和觸發的。
尊重原創,轉載請註明來自:https://www.cnblogs.com/fsjohnhuang/p/16147725.html肥仔John