[vue3] Vue3源碼閱讀筆記 reactivity - collectionHandlers
源碼位置:https://github.com/vuejs/core/blob/main/packages/reactivity/src/collectionHandlers.ts
這個文件主要用于處理Set、Map、WeakSet、WeakMap類型的攔截。
攔截是為了什么?為什么要處理這些方法?
Vue3實現響應式的思路是使用Proxy API在getter中收集依賴,在setter觸發更新。
而Set、Map等這些內置集合類型比較特殊,舉個例子,我們在使用Map的實例對象的時候,我們一般不會在實例對象上面去添加屬性或者修改自定義屬性的值,而是通過其原型上的get/set方法來操作鍵值對。
值得注意的是,我們僅通過調用原型上的方法來操作鍵值對,而不會去修改實例對象上的屬性。因此,我們僅需要給Proxy配置getter,不需要配置setter。
const map = new Map<any, any>();
// √
map.set('k1', 'v1');
map.get('k1');
// ×
map.k1 = 'v1';
map.k1;
而Vue3實現響應式的需求是希望調用get/set方法也能正確地收集依賴、觸發更新。因此,需要對這些方法進行改造。
從響應式原理的角度出發,我們需要思考對集合的讀和寫操作:
- 在讀的時候收集依賴:與讀操作相關的方法,內部要執行
track收集依賴;- 與讀操作相關的方法:
get、has、size(這個是屬性,也要處理)、forEach,以及返回迭代器對象的其它方法;
- 與讀操作相關的方法:
- 在寫的時候觸發更新:與寫操作相關的方法,內部要執行
trigger函數觸發更新;- 與寫操作相關的方法:
add、set、delete、clear。
- 與寫操作相關的方法:
返回迭代器對象的方法有:
const iteratorMethods = [ 'keys', 'values', 'entries', Symbol.iterator, ] as const其中
Symbol.iterator是為了實現for of遍歷必須實現的接口,在JavaScript中的所有可迭代對象都要實現這個接口。
正式開始閱讀代碼
這個文件的代碼結構和baseHandlers不太一樣,這個文件是先分別實現對get、set、has、size等操作的攔截,然后再整合成一個getter返回。
根據是否是shallow和readonly分別導出了四種handler:
mutableCollectionHandlersshallowCollectionHandlersreadonlyCollectionHandlersshallowReadonlyCollectionHandlers
export
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false),
}
export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, true),
}
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(true, false),
}
export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> =
{
get: /*#__PURE__*/ createInstrumentationGetter(true, true),
}
可以看到這些Handlers都是通過createInstrumentationGetter來返回getter,接下來看看createInstrumentationGetter內部是如何實現的。
createInstrumentationGetter
源碼:(分段解析在下面)
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
// 根據是否是只讀和是否是淺層響應式來選擇不同的處理函數集
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations // 淺層只讀的處理函數集
: shallowInstrumentations // 淺層的處理函數集
: isReadonly
? readonlyInstrumentations // 只讀的處理函數集
: mutableInstrumentations // 可變(非只讀)的處理函數集
// 返回一個自定義的getter函數,用于處理特定的鍵
return (
target: CollectionTypes, // 目標集合類型
key: string | symbol, // 被訪問的鍵
receiver: CollectionTypes, // 代理或包裝過的集合
) => {
// 檢查特殊標志鍵
if (key === ReactiveFlags.IS_REACTIVE) {
// 如果不是只讀的,返回true表示是響應式的
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 如果是只讀的,返回true表示是只讀的
return isReadonly
} else if (key === ReactiveFlags.RAW) {
// 返回原始的目標集合
return target
}
// 使用Reflect.get來獲取值
// 如果instrumentations有這個鍵,并且這個鍵在目標集合中,則從instrumentations獲取
// 否則直接從目標集合獲取
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver,
)
}
}
分段解讀代碼:
// 根據是否是只讀和是否是淺層響應式來選擇不同的處理函數集
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations // 淺層只讀的處理函數集
: shallowInstrumentations // 淺層的處理函數集
: isReadonly
? readonlyInstrumentations // 只讀的處理函數集
: mutableInstrumentations // 可變(非只讀)的處理函數集
這個函數根據isReadonly和isShallow選擇了不同的函數集,函數集里的函數是特殊處理過的,目的是為了使這些實例方法可以適應Vue的響應式系統。
// 檢查特殊標志鍵
if (key === ReactiveFlags.IS_REACTIVE) {
// 如果不是只讀的,返回true表示是響應式的
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 如果是只讀的,返回true表示是只讀的
return isReadonly
} else if (key === ReactiveFlags.RAW) {
// 返回原始的目標集合
return target
}
對于Vue內部特有的key,比如ReactiveFlags,返回特定的內容。這些ReactiveFlags并不存在于對象上,只是在getter做攔截并返回。
// 使用Reflect.get來獲取值
// 如果instrumentations有這個鍵,并且這個鍵在目標集合中,則從instrumentations獲取
// 否則直接從目標集合獲取
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver,
)
最后,使用Reflect.get方法執行對key的訪問并返回。這個時候會通過hasOwn(instrumentations, key)檢查訪問key是否在生成的函數集里:
- 如果存在,那么應該應用特殊處理過的函數集里的函數;
- 如果不存在,那么就用
target身上原始的方法。
createInstrumentations
四種不同的函數集由createInstrumentations函數創建并返回。
const [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations,
] = /* #__PURE__*/ createInstrumentations()
接下來是craeteInstrumentations的實現,是一段很長的代碼:
這里只展示了mutableInstrucmentations的函數集,其它三個大同小異,其中的各種零碎的get、size、has...方法的處理在后文介紹。
function createInstrumentations() {
// 定義可變(非只讀)的響應式處理函數集
const mutableInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
// 獲取Map中的值,默認不是只讀且不是淺層
return get(this, key)
},
get size() {
// 獲取集合的大小
return size(this as unknown as IterableCollections)
},
has, // 檢查集合中是否存在特定的值
add, // 向集合中添加元素
set, // 設置Map中的鍵值對
delete: deleteEntry, // 從集合中刪除元素
clear, // 清空集合
forEach: createForEach(false, false), // 遍歷集合的元素
}
// 定義淺層的響應式處理函數集
const shallowInstrumentations: Instrumentations = {
...
}
// 定義只讀的響應式處理函數集
const readonlyInstrumentations: Instrumentations = {
...
}
// 定義淺層只讀的響應式處理函數集
const shallowReadonlyInstrumentations: Instrumentations = {
...
}
// 定義迭代器方法列表
const iteratorMethods = [
'keys',
'values',
'entries',
Symbol.iterator,
] as const
// 為每個迭代器方法添加對應的響應式處理函數
iteratorMethods.forEach(method => {
mutableInstrumentations[method] = createIterableMethod(method, false, false)
readonlyInstrumentations[method] = createIterableMethod(method, true, false)
shallowInstrumentations[method] = createIterableMethod(method, false, true)
shallowReadonlyInstrumentations[method] = createIterableMethod(
method,
true,
true,
)
})
// 返回包含所有處理函數集的數組
return [
mutableInstrumentations, // 可變(非只讀)的處理函數集
readonlyInstrumentations, // 只讀的處理函數集
shallowInstrumentations, // 淺層的處理函數集
shallowReadonlyInstrumentations, // 淺層只讀的處理函數集
]
}
注意到迭代器方法也都做了特殊處理,這是因為迭代器方法返回迭代器對象,而不是操作對象本身,無法被Proxy攔截,故無法追蹤依賴。
這里使用了createIterableMethod創建能夠適配響應式的版本。
createIterableMethod
返回迭代器對象的幾個需要處理的方法分別是:
keysvaluesentriesSymbol.iterator
前三個是string類型傳入,最后一個是symbol類型傳入。
源碼:
function createIterableMethod(
method: string | symbol, // 迭代器方法名,可以是字符串或符號
isReadonly: boolean, // 是否為只讀的迭代器
isShallow: boolean, // 是否為淺層迭代器
) {
// 返回一個自定義的迭代器方法
return function (
this: IterableCollections, // 當前的集合對象
...args: unknown[] // 方法調用的參數
): Iterable<unknown> & Iterator<unknown> {
// 獲取原始的集合對象
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget) // 判斷目標是否為Map類型
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap) // 判斷是否為鍵值對迭代
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() // 調用內部迭代器的next方法
return done
? { value, done } // 如果迭代完成,返回當前值和完成標志
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), // 如果迭代未完成,返回包裝過的值
done, // 完成標志保持不變
}
},
// 實現可迭代協議
[Symbol.iterator]() {
return this
},
}
}
}
Vue3在處理Map和Set的時候并沒有分開處理,而是一起處理了,因為它們有許多名字相同的方法,分開處理可能會導致代碼更亂。
對于entries的輸出,也就是[key, value]格式的遍歷,通過簡單的判斷處理了:
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
方法處理
這里僅記錄get和set方法。
get
get方法是Map和WeakMap獨有的,所以target類型是MapTypes。
查詢的target和key都可能是響應式對象,都需要做toRaw獲取原始值。如果直接在響應式對象上做操作,則可能被Proxy捕獲到,從而記錄了不必要的依賴。
返回值的時候需要根據target的類型進行對應的包裝,即toReactive、toShallow或toReadonly。
這是因為使用set的時候存的是rawValue,而返回的時候需要配合target的類型。
源碼:
function get(
target: MapTypes, // 目標對象,類型是 MapTypes
key: unknown, // 要獲取值的鍵,類型是 unknown
isReadonly = false, // 是否只讀,默認值為 false
isShallow = false // 是否淺層響應,默認值為 false
) {
// 確保如果 target 是響應式對象,操作的是它的原始對象
target = (target as any)[ReactiveFlags.RAW]
// 獲取 target 的原始對象
const rawTarget = toRaw(target)
// 獲取 key 的原始值
const rawKey = toRaw(key)
if (!isReadonly) {
// 如果 key 與 rawKey 不同(即 key 是響應式對象),跟蹤對 key 的訪問
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
// 跟蹤對 rawKey 的訪問
track(rawTarget, TrackOpTypes.GET, rawKey)
}
// 獲取 target 原型上的 has 方法
const { has } = getProto(rawTarget)
// 根據 isShallow 和 isReadonly 選擇對應的包裝函數
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// 如果原始對象上存在 key,則返回包裝后的值
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
// 如果原始對象上存在 rawKey,則返回包裝后的值
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
// 如果 target 不是原始對象,則調用 target.get(key) 進行跟蹤
} else if (target !== rawTarget) {
// 確保在只讀的響應式 Map 中,嵌套的響應式 Map 也能進行依賴跟蹤
target.get(key)
}
}
最后為什么還要加一個判斷target!==rawTarget?
這個判斷和一個bug有關:readonly() breaks reactivity of Map · Issue #3602 · vuejs/core (github.com)
背景:
在 Vue3 的響應式系統中,
readonly和reactive組合使用時可能會出現一些問題,特別是在處理嵌套結構時。例如,當你有一個readonly包裝的reactive Map,并試圖在這個Map中獲取一個值,如果不進行額外處理,可能會導致嵌套的響應式Map無法正確進行依賴跟蹤。示例代碼:
const reactiveMap = reactive(new Map([['key', new Map([['nestedKey', 'value']])]])); const readonlyMap = readonly(reactiveMap); // 獲取嵌套的 Map const nestedMap = readonlyMap.get('key'); // 嘗試獲取嵌套 Map 的值 const value = nestedMap.get('nestedKey');在這種情況下,如果不進行額外處理,
nestedMap可能無法正確進行依賴跟蹤。因為直接操作readonly包裝的對象不會觸發響應式系統的依賴跟蹤。這意味著當nestedKey的值發生變化時,可能不會觸發相關的響應式更新。解決方法:
判斷
target是否是響應式對象,如果是的話,手動調用get觸發對依賴的收集。注意到
rawTarget是由toRaw(target)得到的,接下來看一下toRaw函數的實現:
toRaw的源碼位置:core/packages/reactivity/src/reactive.ts at main · vuejs/core (github.com)export function toRaw<T>(observed: T): T { // 嘗試獲取raw對象 const raw = observed && (observed as Target)[ReactiveFlags.RAW] // 如果存在raw對象,則遞歸調用;如果不存在,則表示當前的observed已經是原始對象 return raw ? toRaw(raw) : observed }可以看到如果傳入的對象如果有
ReactiveFlags.RAW這個key,就認為它是被Vue包裝過的對象,因為只有被reactive、readonly等API包裝過的對象會被Vue添加上ReactiveFlags.RAW屬性,記錄著原始對象的引用。這里需要遞歸調用是因為對象可能被多層包裝,比如
readonly(reactive({}))。回到Map的get方法的最后處理:
if (target !== rawTarget) { // 確保在只讀的響應式 Map 中,嵌套的響應式 Map 也能進行依賴跟蹤 target.get(key) }
如果
target === rawTarget,則target是原始對象;如果
target!==rawTarget,則target是包裝過的對象,可能是reactive包裝過的響應式對象,也可能是readonly包裝過的只讀對象;這里或許可以再優化?如果是只讀對象,就不追蹤依賴了。
set
Map的key可能是原始值也可能是響應式對象,這里需要做類型判斷,并且對原始key和響應式key都做判斷。
在開發環境下如果存在同一個原始對象的兩種類型的key,會輸出警告。
因為這種不規范的寫法會保存兩份鍵值對,內容可能不一致。
源碼:
function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) {
// 如果值不是淺層的且不是只讀的,則獲取其原始值
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
// 獲取目標對象的原始對象
const target = toRaw(this)
const { has, get } = getProto(target)
// 檢查目標對象是否已經存在該鍵
let hadKey = has.call(target, key)
if (!hadKey) {
// 如果不存在,嘗試使用原始鍵進行再次檢查
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
// 在開發環境中,檢查鍵的類型是否一致
checkIdentityKeys(target, has, key)
}
// 獲取舊值
const oldValue = get.call(target, key)
// 設置新值
target.set(key, value)
// 觸發依賴追蹤
if (!hadKey) {
// 如果鍵之前不存在,觸發添加操作的依賴
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 如果鍵之前存在且值發生了變化,觸發設置操作的依賴
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
// 返回 this 以支持鏈式調用
return this
}

浙公網安備 33010602011771號