聽說你很了解 Vue3 響應(yīng)式?
前言
<< 溫馨提醒 >>
本文內(nèi)容偏干,建議邊喝水邊食用,如有不適請及時點贊!
【A】:能不能說說 Vue3 響應(yīng)式都處理了哪些數(shù)據(jù)類型?都怎么處理的呀?
【B】:能,只能說一點點...

【A】:...
只要問到 Vue 相關(guān)的內(nèi)容,似乎總繞不過 響應(yīng)式原理 的話題,隨之而來的回答必然是圍繞著 Object.defineProperty 和 Proxy 來展開(即 Vue2 和 Vue3),但若繼續(xù)追問某些具體實現(xiàn)是不是就倉促結(jié)束回答了(~~你跑我追,你不跑我還追~~)。
本文就不再過多介紹 Vue2 中響應(yīng)式的處理,感興趣可以參考 **從 vue 源碼看問題 —— 如何理解 `Vue` 響應(yīng)式?**[2],但是會有簡單提及,下面就來看看 Vue3 中是如何處理 原始值、Object、Array、Set、Map 等數(shù)據(jù)類型的響應(yīng)式。

從 Object.defineProperty 到 Proxy
一切的一切還得從 Object.defineProperty 開始講起,那是一個不一樣的 API ...
(~~bgm 響起,自行體會~~)
Object.defineProperty
Object.defineProperty(obj, prop, descriptor) 方法會直接在一個對象上定義一個 新屬性,或修改一個 對象 的 現(xiàn)有屬性,并返回此對象,其參數(shù)具體為:
obj:要定義屬性的對象prop:要定義或修改的 屬性名稱 或 **`Symbol`**[3]descriptor:要定義或修改的 屬性描述符
從以上的描述就可以看出一些限制,比如:
目標是 對象屬性,不是 整個對象 一次只能 定義或修改一個屬性 當然有對應(yīng)的一次處理多個屬性的方法 `Object.defineProperties()`[4],但在 vue中并不適用,因為vue不能提前知道用戶傳入的對象都有什么屬性,因此還是得經(jīng)過類似Object.keys() + for循環(huán)的方式獲取所有的key -> value,而這其實是沒有必要使用 `Object.defineProperties()`[5]
在 Vue2 中的缺陷
Object.defineProperty() 實際是通過 定義 或 修改 對象屬性 的描述符來實現(xiàn) 數(shù)據(jù)劫持,其對應(yīng)的缺點也是沒法被忽略的:
只能攔截對象屬性的 get和set操作,比如無法攔截delete、in、方法調(diào)用等操作動態(tài)添加新屬性(響應(yīng)式丟失) 保證后續(xù)使用的屬性要在初始化聲明 data時進行定義使用 this.$set()設(shè)置新屬性通過 delete刪除屬性(響應(yīng)式丟失)使用 this.$delete()刪除屬性使用數(shù)組索引 替換/新增 元素(響應(yīng)式丟失) 使用 this.$set()設(shè)置新元素使用數(shù)組 push、pop、shift、unshift、splice、sort、reverse等 原生方法 改變原數(shù)組時(響應(yīng)式丟失)使用 重寫/增強 后的 push、pop、shift、unshift、splice、sort、reverse方法一次只能對一個屬性實現(xiàn) 數(shù)據(jù)劫持,需要遍歷對所有屬性進行劫持 數(shù)據(jù)結(jié)構(gòu)復(fù)雜時(屬性值為 引用類型數(shù)據(jù)),需要通過 遞歸 進行處理
【擴展】Object.defineProperty 和 Array ?
它們有啥關(guān)系,其實沒有啥關(guān)系,只是大家習慣性的會回答 Object.defineProperty 不能攔截 Array 的操作,這句話說得對但也不對
。
使用 Object.defineProperty 攔截 Array
Object.defineProperty 可用于實現(xiàn)對象屬性的 get 和 set 攔截,而數(shù)組其實也是對象,那自然是可以實現(xiàn)對應(yīng)的攔截操作,如下:

Vue2 為什么不使用 Object.defineProperty 攔截 Array?
尤大在曾在 GitHub 的 Issue 中做過如下回復(fù):

說實話性能問題到底指的是什么呢?
下面是總結(jié)了一些目前看到過的回答:
數(shù)組 和 普通對象 在使用場景下有區(qū)別,在項目中使用數(shù)組的目的大多是為了 遍歷,即比較少會使用 array[index] = xxx的形式,更多的是使用數(shù)組的Api的方式數(shù)組長度是多變的,不可能像普通對象一樣先在 data選項中提前聲明好所有元素,比如通過array[index] = xxx方式賦值時,一旦index的值超過了現(xiàn)有的最大索引值,那么當前的添加的新元素也不會具有響應(yīng)式數(shù)組存儲的元素比較多,不可能為每個數(shù)組元素都設(shè)置 getter/setter無法攔截數(shù)組原生方法如 push、pop、shift、unshift等的調(diào)用,最終仍需 重寫/增強 原生方法
Proxy & Reflect
由于在 Vue2 中使用 Object.defineProperty 帶來的缺陷,導(dǎo)致在 Vue2 中不得不提供了一些額外的方法(如:Vue.set、Vue.delete())解決問題,而在 Vue3 中使用了 Proxy 的方式來實現(xiàn) 數(shù)據(jù)劫持,而上述的問題在 Proxy 中都可以得到解決。
Proxy
**`Proxy`**[6] 主要用于創(chuàng)建一個 對象的代理,從而實現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等),本質(zhì)上是通過攔截對象 內(nèi)部方法 的執(zhí)行實現(xiàn)代理,而對象本身根據(jù)規(guī)范定義的不同又會區(qū)分為 常規(guī)對象 和 異質(zhì)對象(這不是重點,可自行了解)。
new Proxy(target, handler)是針對整個對象進行的代理,不是某個屬性代理對象屬性擁有 讀取、修改、刪除、新增、是否存在屬性 等操作相應(yīng)的捕捉器,**更多可見**[7] `get()`[8] 屬性 讀取 操作的捕捉器 `set()`[9] 屬性 設(shè)置 操作的捕捉器 `deleteProperty()`[10] 是 `delete`[11] 操作符的捕捉器 `ownKeys()`[12] 是 `Object.getOwnPropertyNames`[13] 方法和 `Object.getOwnPropertySymbols`[14] 方法的捕捉器 `has()`[15] 是 `in`[16] 操作符的捕捉器
Reflect
**`Reflect`**[17] 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法,這些方法與 **`Proxy handlers`**[18] 提供的的方法是一一對應(yīng)的,且 Reflect 不是一個函數(shù)對象,即不能進行實例化,其所有屬性和方法都是靜態(tài)的。
`Reflect.get(target, propertyKey[, receiver])`[19] 獲取對象身上某個屬性的值,類似于 target[name]`Reflect.set(target, propertyKey, value[, receiver])`[20] 將值分配給屬性的函數(shù)。返回一個`Boolean`[21],如果更新成功,則返回 true`Reflect.deleteProperty(target, propertyKey)`[22] 作為函數(shù)的`delete`[23]操作符,相當于執(zhí)行 delete target[name]`Reflect.ownKeys(target)`[24] 返回一個包含所有自身屬性(不包含繼承屬性)的數(shù)組。(類似于 `Object.keys()`[25], 但不會受 enumerable影響)`Reflect.has(target, propertyKey)`[26] 判斷一個對象是否存在某個屬性,和 `in` 運算符[27] 的功能完全相同
**更多方法點此可見**[28]
Proxy 為什么需要 Reflect 呢?
在 Proxy 的 get(target, key, receiver)、set(target, key, newVal, receiver) 的捕獲器中都能接到前面所列舉的參數(shù):
target指的是 原始數(shù)據(jù)對象key指的是當前操作的 屬性名newVal指的是當前操作接收到的 最新值receiver指向的是當前操作 正確的上下文
怎么理解 Proxy handler 中 receiver 指向的是當前操作正確上的下文呢?
正常情況下,**
receiver** 指向的是 當前的代理對象
image.png 特殊情況下,**
receiver** 指向的是 引發(fā)當前操作的對象
image.png 通過 Object.setPrototypeOf()方法將代理對象proxy設(shè)置為普通對象obj的原型通過 obj.name訪問其不存在的name屬性,由于原型鏈的存在,最終會訪問到proxy.name上,即觸發(fā)get捕獲器
在 Reflect 的方法中通常只需要傳遞 target、key、newVal 等,但為了能夠處理上述提到的特殊情況,一般也需要傳遞 receiver 參數(shù),因為 Reflect 方法中傳遞的 receiver 參數(shù)代表執(zhí)行原始操作時的 this 指向,比如:Reflect.get(target, key , receiver)、Reflect.set(target, key, newVal, receiver)。
總結(jié):**Reflect** 是為了在執(zhí)行對應(yīng)的攔截操作的方法時能 傳遞正確的 this 上下文。
Vue3 如何使用 Proxy 實現(xiàn)數(shù)據(jù)劫持?
Vue3 中提供了 reactive() 和 ref() 兩個方法用來將 目標數(shù)據(jù) 變成 響應(yīng)式數(shù)據(jù),而通過 Proxy 來實現(xiàn) 數(shù)據(jù)劫持(或代理) 的具體實現(xiàn)就在其中,下面一起來看看吧!

reactive 函數(shù)
從源碼來看,其核心其實就是 createReactiveObject(...) 函數(shù),那么繼續(xù)往下查看對應(yīng)的內(nèi)容
源碼位置:packages\reactivity\src\reactive.ts
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 若目標對象是響應(yīng)式的只讀數(shù)據(jù),則直接返回
if (isReadonly(target)) {
return target
}
// 否則將目標數(shù)據(jù)嘗試變成響應(yīng)式數(shù)據(jù)
return createReactiveObject(
target,
false,
mutableHandlers, // 對象類型的 handlers
mutableCollectionHandlers, // 集合類型的 handlers
reactiveMap
)
}
復(fù)制代碼
createReactiveObject() 函數(shù)
源碼的體現(xiàn)也是非常簡單,無非就是做一些前置判斷處理:
若目標數(shù)據(jù)是 原始值類型,直接向返回 原數(shù)據(jù) 若目標數(shù)據(jù)的 __v_raw屬性為true,且是【非響應(yīng)式數(shù)據(jù)】或 不是通過調(diào)用readonly()方法,則直接返回 原數(shù)據(jù)若目標數(shù)據(jù)已存在相應(yīng)的 proxy代理對象,則直接返回 對應(yīng)的代理對象若目標數(shù)據(jù)不存在對應(yīng)的 白名單數(shù)據(jù)類型 中,則直接返回原數(shù)據(jù),支持響應(yīng)式的數(shù)據(jù)類型如下: 可擴展的對象,即是否可以在它上面添加新的屬性 __v_skip 屬性不存在或值為 false 的對象 數(shù)據(jù)類型為 Object、Array、Map、Set、WeakMap、WeakSet的對象其他數(shù)據(jù)都統(tǒng)一被認為是 無效的響應(yīng)式數(shù)據(jù)對象 通過 Proxy創(chuàng)建代理對象,根據(jù)目標數(shù)據(jù)類型選擇不同的Proxy handlers
看來具體的實現(xiàn)又在不同數(shù)據(jù)類型的 捕獲器 中,即下面源碼的 collectionHandlers 和 baseHandlers ,而它們則對應(yīng)的是在上述 reactive() 函數(shù)中為 createReactiveObject() 函數(shù)傳遞的 mutableCollectionHandlers 和 mutableHandlers 參數(shù)。
源碼位置:packages\reactivity\src\reactive.ts
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 非對象類型直接返回
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 目標數(shù)據(jù)的 __v_raw 屬性若為 true,且是【非響應(yīng)式數(shù)據(jù)】或 不是通過調(diào)用 readonly() 方法,則直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 目標對象已存在相應(yīng)的 proxy 代理對象,則直接返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有在白名單中的值類型才可以被代理監(jiān)測,否則直接返回
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 創(chuàng)建代理對象
const proxy = new Proxy(
target,
// 若目標對象是集合類型(Set、Map)則使用集合類型對應(yīng)的捕獲器,否則使用基礎(chǔ)捕獲器
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 將對應(yīng)的代理對象存儲在 proxyMap 中
proxyMap.set(target, proxy)
return proxy
}
復(fù)制代碼
捕獲器 Handlers
對象類型的捕獲器 — mutableHandlers
這里的對象類型指的是 數(shù)組 和 普通對象
源碼位置:packages\reactivity\src\baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
復(fù)制代碼
以上這些捕獲器其實就是我們在上述 Proxy 部分列舉出來的捕獲器,顯然可以攔截對普通對象的如下操作:
讀取,如 obj.name設(shè)置,如 obj.name = 'zs'刪除屬性,如 delete obj.name判斷是否存在對應(yīng)屬性,如 name in obj獲取對象自身的屬性值,如 obj.getOwnPropertyNames()和obj.getOwnPropertySymbols()
get 捕獲器
具體信息在下面的注釋中,這里只列舉核心內(nèi)容:
若當前數(shù)據(jù)對象是 數(shù)組,則 重寫/增強 數(shù)組對應(yīng)的方法 數(shù)組元素的 查找方法: includes、indexOf、lastIndexOf修改原數(shù)組 的方法: push、pop、unshift、shift、splice若當前數(shù)據(jù)對象是 普通對象,且非 只讀 的則通過 track(target, TrackOpTypes.GET, key)進行 依賴收集若當前數(shù)據(jù)對象是 淺層響應(yīng) 的,則直接返回其對應(yīng)屬性值 若當前數(shù)據(jù)對象是 ref 類型的,則會進行 自動脫 ref 若當前數(shù)據(jù)對象的屬性值是 對象類型 若當前屬性值屬于 只讀的,則通過 readonly(res)向外返回其結(jié)果否則會將當前屬性值以 reactive(res)向外返回 proxy 代理對象否則直接向外返回對應(yīng)的 屬性值
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 當直接通過指定 key 訪問 vue 內(nèi)置自定義的對象屬性時,返回其對應(yīng)的值
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
// 判斷是否為數(shù)組類型
const targetIsArray = isArray(target)
// 數(shù)組對象
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 重寫/增強數(shù)組的方法:
// - 查找方法:includes、indexOf、lastIndexOf
// - 修改原數(shù)組的方法:push、pop、unshift、shift、splice
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 獲取對應(yīng)屬性值
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 依賴收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 淺層響應(yīng)
if (shallow) {
return res
}
// 若是 ref 類型響應(yīng)式數(shù)據(jù),會進行【自動脫 ref】,但不支持【數(shù)組】+【索引】的訪問方式
if (isRef(res)) {
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
// 屬性值是對象類型:
// - 是只讀屬性,則通過 readonly() 返回結(jié)果,
// - 且是非只讀屬性,則遞歸調(diào)用 reactive 向外返回 proxy 代理對象
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
復(fù)制代碼
set 捕獲器
除去額外的邊界處理,其實核心還是 更新屬性值,并通過 trigger(...) 觸發(fā)依賴更新
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 保存舊的數(shù)據(jù)
let oldValue = (target as any)[key]
// 若原數(shù)據(jù)值屬于 只讀 且 ref 類型,并且新數(shù)據(jù)值不屬于 ref 類型,則意味著修改失敗
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!shallow && !isReadonly(value)) {
if (!isShallow(value)) {
value = toRaw(value)
oldValue = toRaw(oldValue)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 是否存在對應(yīng)的 key
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 設(shè)置對應(yīng)值
const result = Reflect.set(target, key, value, receiver)
// 若目標對象是原始原型鏈上的內(nèi)容(非自定義添加),則不觸發(fā)依賴更新
if (target === toRaw(receiver)) {
if (!hadKey) {
// 目標對象不存在對應(yīng)的 key,則為新增操作
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 目標對象存在對應(yīng)的值,則為修改操作
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
// 返回修改結(jié)果
return result
}
}
復(fù)制代碼
deleteProperty & has & ownKeys 捕獲器
這三個捕獲器內(nèi)容非常簡潔,其中 has 和 ownKeys 本質(zhì)也屬于 讀取操作,因此需要通過 track() 進行依賴收集,而 deleteProperty 相當于修改操作,因此需要 trigger() 觸發(fā)更新
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
// 目標對象上存在對應(yīng)的 key ,并且能成功刪除,才會觸發(fā)依賴更新
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
function ownKeys(target: object): (string | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
復(fù)制代碼
數(shù)組類型捕獲器 —— arrayInstrumentations
數(shù)組類型 和 對象類型 的大部分操作是可以共用的,比如 obj.name 和 arr[index] 等,但數(shù)組類型的操作還是會比對象類型更豐富一些,而這些就需要特殊處理。
源碼位置:
packages\reactivity\src\collectionHandlers.ts
處理數(shù)組索引 index 和 length
數(shù)組的 index 和 length 是會相互影響的,比如存在數(shù)組 const arr = [1] :
arr[1] = 2的操作會隱式修改length的屬性值arr.length = 0的操作會導(dǎo)致原索引位的值發(fā)生變更
為了能夠合理觸發(fā)和 length 相關(guān)副作用函數(shù)的執(zhí)行,在 set() 捕獲器中會判斷當前操作的類型:
當 Number(key) < target.length證明是修改操作,對應(yīng)TriggerOpTypes.SET類型,即當前操作不會改變length的值,不需要 觸發(fā)和length相關(guān)副作用函數(shù)的執(zhí)行當 Number(key) >= target.length證明是新增操作,TriggerOpTypes.ADD類型,即當前操作會改變length的值,需要 觸發(fā)和length相關(guān)副作用函數(shù)的執(zhí)行
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
省略其他代碼
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
復(fù)制代碼
處理數(shù)組的查找方法
數(shù)組的查找方法包括 includes、indexOf、lastIndexOf,這些方法通常情況下是能夠按預(yù)期進行工作,但還是需要對某些特殊情況進行處理:
當查找的目標數(shù)據(jù)是響應(yīng)式數(shù)據(jù)本身時,得到的就不是預(yù)期結(jié)果
const obj = {}
const proxy = reactive([obj])
console.log(proxy.includs(proxy[0])) // false
復(fù)制代碼【產(chǎn)生原因】首先這里涉及到了兩次讀取操作,第一次 是 proxy[0]此時會觸發(fā)get捕獲器并為obj生成對應(yīng)代理對象并返回,第二次 是proxy.includs()的調(diào)用,它會遍歷數(shù)組的每個元素,即會觸發(fā)get捕獲器,并又生成一個新的代理對象并返回,而這兩次生成的代理對象不是同一個,因此返回false【解決方案】源碼中會在 get中設(shè)置一個名為proxyMap的WeakMap集合用于存儲每個響應(yīng)式對象,在觸發(fā)get時優(yōu)先返回proxyMap存在的響應(yīng)式對象,這樣不管觸發(fā)多少次get都能返回相同的響應(yīng)式數(shù)據(jù)當在響應(yīng)式對象中查找原始數(shù)據(jù)時,得到的就不是預(yù)期結(jié)果
const obj = {}
const proxy = reactive([obj])
console.log(proxy.includs(obj)) // false
復(fù)制代碼在 重寫/增強 的 includes、indexOf、lastIndexOf等方法中,會將當前方法內(nèi)部訪問到的響應(yīng)式數(shù)據(jù)轉(zhuǎn)換為原始數(shù)據(jù),然后調(diào)用數(shù)組對應(yīng)的原始方法進行查找,若查找結(jié)果為true則直接返回結(jié)果若以上操作沒有查找到,則通過將當前方法傳入的參數(shù)轉(zhuǎn)換為原始數(shù)據(jù),在調(diào)用數(shù)組的原始方法,此時直接將對應(yīng)的結(jié)果向外進行返回 【產(chǎn)生原因】 proxy.includes()會觸發(fā)get捕獲器并為obj生成對應(yīng)代理對象并返回,而includes方法的參數(shù)傳遞的是 原始數(shù)據(jù),相當于此時是 響應(yīng)式對象 和 原始數(shù)據(jù)對象 進行比較,因此對應(yīng)的結(jié)果一定是為false【解決方案】核心就是將它們的數(shù)據(jù)類型統(tǒng)一,即統(tǒng)一都使用 原始值數(shù)據(jù)對比 或 響應(yīng)式數(shù)據(jù)對比,由于 includes()的方法本身并不支持對傳入?yún)?shù)或內(nèi)部響應(yīng)式數(shù)據(jù)的處理,因此需要自定義以上對應(yīng)的數(shù)組查找方法
源碼位置:packages\reactivity\src\baseHandlers.ts
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 外部調(diào)用上述方法,默認其內(nèi)的 this 指向的是代理數(shù)組對象,
// 但實際上是需要通過原始數(shù)組中進行遍歷查找
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
復(fù)制代碼
處理數(shù)組影響 length 的方法
隱式修改數(shù)組長度的原型方法包括 push、pop、shift、unshift、splice 等,在調(diào)用這些方法的同時會間接的讀取數(shù)組的 length 屬性,又因為這些方法具有修改數(shù)組長度的能力,即相當于 length 的設(shè)置操作,若不進行特殊處理,會導(dǎo)致與 length 屬性相關(guān)的副作用函數(shù)被重復(fù)執(zhí)行,即 棧溢出,比如:
const proxy = reactive([])
// 第一個副作用函數(shù)
effect(() => {
proxy.push(1) // 讀取 + 設(shè)置 操作
})
// 第二個副作用函數(shù)
effect(() => {
proxy.push(2) // 讀取 + 設(shè)置 操作(此時進行 trigger 時,會觸發(fā)包括第一個副作用函數(shù)的內(nèi)容,然后循環(huán)導(dǎo)致棧溢出)
})
復(fù)制代碼
在源碼中還是通過 重寫/增強 上述對應(yīng)數(shù)組方法的形式實現(xiàn)自定義的邏輯處理:
在調(diào)用真正的數(shù)組原型方法前,會通過設(shè)置 pauseTracking()方法來禁止track依賴收集在調(diào)用數(shù)組原生方法后,在通過 resetTracking()方法恢復(fù)track進行依賴收集實際上以上的兩個方法就是通過控制 shouldTrack變量為true或false,使得在track函數(shù)執(zhí)行時是否要執(zhí)行原來的依賴收集邏輯
源碼位置:packages\reactivity\src\baseHandlers.ts
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
復(fù)制代碼
集合類型的捕獲器 — mutableCollectionHandlers
集合類型 包括 Map、WeakMap、Set、WeakSet 等,而對 集合類型 的 代理模式 和 對象類型 需要有所不同,因為 集合類型 和 對象類型 的操作方法是不同的,比如:
Map類型 的原型 屬性 和 方法 如下,**詳情可見**[29]:
size clear() delete(key) has(key) get(key) set(key) keys() values() entries() forEach(cb)
Set類型 的原型 屬性 和 方法 如下,**詳情可見**[30]:
size add(value) clear() delete(value) has(value) keys() values() entries() forEach(cb)
源碼位置:
packages\reactivity\src\collectionHandlers.ts
解決 代理對象 無法訪問 集合類型 對應(yīng)的 屬性 和 方法
代理集合類型的第一個問題,就是代理對象沒法獲取到集合類型的屬性和方法,比如:

從報錯信息可以看出 size 屬性是一個訪問器屬性,所以它被作為方法調(diào)用了,而主要錯誤原因就是在這個訪問器中的 this 指向的是 代理對象,在源碼中就是通過為這些特定的 屬性 和 方法 定義對應(yīng)的 key 的 mutableInstrumentations 對象,并且在其對應(yīng)的 屬性 和 方法 中將 this指向為 原對象.
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
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)
}
function size(target: IterableCollections, isReadonly = false) {
target = (target as any)[ReactiveFlags.RAW]
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
省略其他代碼
復(fù)制代碼
處理集合類型的響應(yīng)式
集合建立響應(yīng)式核心還是 track 和 trigger,轉(zhuǎn)而思考的問題就變成,什么時候需要 track、什么時候需要 trigger:
track時機:get()、get size()、has()、forEach()trigger時機:add()、set()、delete()、clear()
這里涉及一些優(yōu)化的內(nèi)容,比如:
在 add()中通過has()判斷當前添加的元素是否已經(jīng)存在于Set集合中時,若已存在就不需要進行trigger()操作,因為Set集合本身的一個特性就是 去重在 delete()中通過has()判斷當前刪除的元素或?qū)傩允欠翊嬖?,若不存在就不需要進行trigger()操作,因為此時的刪除操作是 無效的
function createInstrumentations() {
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {// track
return get(this, key)
},
get size() {// track
return size(this as unknown as IterableCollections)
},
has,// track
add,// trigger
set,// trigger
delete: deleteEntry,// trigger
clear,// trigger
forEach: createForEach(false, false) // track
}
省略其他代碼
}
復(fù)制代碼
避免污染原始數(shù)據(jù)
通過重寫集合類型的方法并手動指定其中的 this 指向為 原始對象 的方式,解決 代理對象 無法訪問 集合類型 對應(yīng)的 屬性 和 方法 的問題,但這樣的實現(xiàn)方式也帶來了另一個問題:**原始數(shù)據(jù)被污染** 。
簡單來說,我們只希望 代理對象(響應(yīng)式對象) 才具備 依賴收集(track) 和 依賴更新(trigger) 的能力,而通過 原始數(shù)據(jù) 進行的操作不應(yīng)該具有響應(yīng)式的能力。
如果只是單純的把所有操作直接作用到 原始對象 上就不能保證這個結(jié)果,比如:

// 原數(shù)數(shù)據(jù) originalData1
const originalData1 = new Map({});
// 代理對象 proxyData1
const proxyData1 = reactive(originalData1);
// 另一個代理對象 proxyData2
const proxyData2 = reactive(new Map({}));
// 將 proxyData2 做為 proxyData1 一個鍵值
// 【注意】此時的 set() 經(jīng)過重寫,其內(nèi)部 this 已經(jīng)指向 原始對象(originalData1),等價于 原始對象 originalData1 上存儲了一個 響應(yīng)式對象 proxyData2
proxyData1.set("proxyData2", proxyData2);
// 若不做額外處理,如下基于 原始數(shù)據(jù)的操作 就會觸發(fā) track 和 trigger
originalData1.get("proxyData2").set("name", "zs");
復(fù)制代碼
在源碼中的解決方案也是很簡單,直接通過 value = toRaw(value) 獲取當前設(shè)置值對應(yīng)的 原始數(shù)據(jù),這樣舊可以避免 響應(yīng)式數(shù)據(jù)對原始數(shù)據(jù)的污染。
處理 forEach 回調(diào)參數(shù)
首先 `Map.prototype.forEach(callbackFn [, thisArg])`[31] 其中 callbackFn 回調(diào)函數(shù)會接收三個參數(shù):
當前的 值 value當前的 鍵 key正在被遍歷的 Map對象(原始對象)
遍歷操作 等價于 讀取操作,在處理 普通對象 的 get() 捕獲器中有一個處理,如果當前訪問的屬性值是 對象類型 那么就會向外返回其對應(yīng)的 代理對象,目的是實現(xiàn) 惰性響應(yīng) 和 深層響應(yīng),這個處理也同樣適用于 集合類型。
因此,在源碼中通過 callback.call(thisArg, wrap(value), wrap(key), observed) 的方式將 Map 類型的 鍵 和 值 進行響應(yīng)式處理,以及進行 track 操作,因為 Map 類型關(guān)注的就是 鍵 和 值。
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) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
復(fù)制代碼
處理迭代器
集合類型的迭代器方法:
entries()keys()values()
Map 和 Set 都實現(xiàn)了 可迭代協(xié)議(即 Symbol.iterator 方法,而 迭代器協(xié)議 是指 一個對象實現(xiàn)了 next 方法),因此它們還可以通過 for...of 的方式進行遍歷。
根據(jù)對 forEach 的處理,不難知道涉及遍歷的方法,終究還是得將其對應(yīng)的遍歷的 鍵、值 進行響應(yīng)式包裹的處理,以及進行 track 操作,而原本的的迭代器方法沒辦法實現(xiàn),因此需要內(nèi)部自定義迭代器協(xié)議。
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
省略其他代碼
})
復(fù)制代碼
這一部分的源碼涉及的內(nèi)容比較多,以上只是簡單的總結(jié)一下,更詳細的內(nèi)容可查看對應(yīng)的源碼內(nèi)容。
ref 函數(shù) — 原始值的響應(yīng)式
原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined、null 等類型的值,我們知道用 Object.defineProperty 肯定是不支持,因為它攔截的就是對象屬性的操作,都說 Proxy 比 Object.defineProperty 強,那么它能不能直接支持呢?
直接支持是肯定不能的,別忘了 Proxy 代理的目標也還是對象類型呀,它的強是在自己的所屬領(lǐng)域,跨領(lǐng)域也是遭不住的。

因此在 Vue3 的 ref 函數(shù)中對原始值的處理方式是通過為 原始值類型 提供一個通過 new RefImpl(rawValue, shallow) 實例化得到的 包裹對象,說白了還是將原始值類型變成對象類型,但 ref 函數(shù)的參數(shù)并 不限制數(shù)據(jù)類型:
原始值類型, ref函數(shù)中會為原始值類型數(shù)據(jù)創(chuàng)建RefImpl實例對象(必須通過.value的方式訪問數(shù)據(jù)),并且實現(xiàn)自定義的get、set用于分別進行 依賴收集 和 依賴更新,注意的是這里并不會通過Proxy為原始值類型創(chuàng)建代理對象,準確的說在RefImpl內(nèi)部自定義實現(xiàn)的get、set就實現(xiàn)了對原始值類型的攔截操作,**因為原始值類型不需要向?qū)ο箢愋驮O(shè)置那么多的捕獲器**對象類型, ref函數(shù)中除了為 對象類型 數(shù)據(jù)創(chuàng)建RefImpl實例對象之外,還會通過reactive函數(shù)將其轉(zhuǎn)換為響應(yīng)式數(shù)據(jù),其實主要還是為了支持類似如下的操作const refProxy = ref({name: 'zs'})
refProxy.value.name = 'ls'
復(fù)制代碼依賴容器 dep,在 ref類型中依賴存儲的位置就是每個ref實例對象上的dep屬性,它本質(zhì)就是一個Set實例,觸發(fā)get時往dep中添加副作用函數(shù)(依賴),觸發(fā)set時從dep中依次取出副作用函數(shù)執(zhí)行
源碼位置:packages\reactivity\src\ref.ts
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 將依賴收集到 dep 中,實際上就是一個 Set 類型
trackRefValue(this)
return this._value
}
set value(newVal) {
// 獲取原始數(shù)據(jù)
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 通過 Object.is(value, oldValue) 判斷新舊值是否一致,若不一致才需要進行更新
if (hasChanged(newVal, this._rawValue)) {
// 保存原始值
this._rawValue = newVal
// 更新為新的 value 值
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 依賴更新,從 dep 中取出對應(yīng)的 effect 函數(shù)依次遍歷執(zhí)行
triggerRefValue(this, newVal)
}
}
}
// 若當前 value 是 對象類型,才會通過 reactive 轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
復(fù)制代碼
Vue3 如何進行依賴收集?
在 Vue2 中依賴的收集方式是通過 Dep 和 Watcher 的 觀察者模式 來實現(xiàn)的,是不是還能想起初次了解 Dep 和 Watcher 之間的這種 剪不斷理還亂 的關(guān)系時的心情 ......
關(guān)于 設(shè)計模式 部分感興趣可查看 **常見 JavaScript 設(shè)計模式 — 原來這么簡單**[32] 一文,里面主要圍繞著
Vue中對應(yīng)的設(shè)計模式來進行介紹,相信會有一定的幫助
依賴收集 其實說的就是 track 函數(shù)需要處理的內(nèi)容:
聲明 targetMap作為一個容器,用于保存和當前響應(yīng)式對象相關(guān)的依賴內(nèi)容,本身是一個WeakMap類型選擇 WeakMap類型作為容器,是因為WeakMap對 鍵(對象類型)的引用是 弱類型 的,一旦外部沒有對該 鍵(對象類型)保持引用時,WeakMap就會自動將其刪除,即 能夠保證該對象能夠正常被垃圾回收而 Map類型對 鍵 的引用則是 強引用 ,即便外部沒有對該對象保持引用,但至少還存在Map本身對該對象的引用關(guān)系,因此會導(dǎo)致該對象不能及時的被垃圾回收將對應(yīng)的 響應(yīng)式數(shù)據(jù)對象 作為 targetMap的 鍵,存儲和當前響應(yīng)式數(shù)據(jù)對象相關(guān)的依賴關(guān)系depsMap(屬于Map實例),即depsMap存儲的就是和當前響應(yīng)式對象的每一個key對應(yīng)的具體依賴將 deps(屬于Set實例)作為depsMap每個key對應(yīng)的依賴集合,因為每個響應(yīng)式數(shù)據(jù)可能在多個副作用函數(shù)中被使用,并且Set類型用于自動去重的能力
可視化結(jié)構(gòu)如下:

源碼位置:packages\reactivity\src\effect.ts
const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 當前應(yīng)該進行依賴收集 且 有對應(yīng)的副作用函數(shù)時,才會進行依賴收集
if (shouldTrack && activeEffect) {
// 從容器中取出【對應(yīng)響應(yīng)式數(shù)據(jù)對象】的依賴關(guān)系
let depsMap = targetMap.get(target)
if (!depsMap) {
// 若不存在,則進行初始化
targetMap.set(target, (depsMap = new Map()))
}
// 獲取和對【應(yīng)響應(yīng)式數(shù)據(jù)對象 key】相匹配的依賴
let dep = depsMap.get(key)
if (!dep) {
// 若不存在,則進行初始化 dep 為 Set 實例
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
// 往 dep 集合中添加 effect 依賴
trackEffects(dep, eventInfo)
}
}
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
dep.w = 0
dep.n = 0
return dep
}
復(fù)制代碼
最后
以上就是針對 Vue3 中對不同數(shù)據(jù)類型的處理的內(nèi)容,無論是 Vue2 還是 Vue3 響應(yīng)式的核心都是 數(shù)據(jù)劫持/代理、依賴收集、依賴更新,只不過由于實現(xiàn)數(shù)據(jù)劫持方式的差異從而導(dǎo)致具體實現(xiàn)的差異,在 Vue3 中值得注意的是:
普通對象類型 可以直接配合 Proxy提供的捕獲器實現(xiàn)響應(yīng)式數(shù)組類型 也可以直接復(fù)用大部分和 普通對象類型 的捕獲器,但其對應(yīng)的查找方法和隱式修改 length的方法仍然需要被 重寫/增強為了支持 集合類型 的響應(yīng)式,也對其對應(yīng)的方法進行了 重寫/增強 原始值數(shù)據(jù)類型 主要通過 ref函數(shù)來進行響應(yīng)式處理,不過內(nèi)容不會對 原始值類型 使用reactive(或 Proxy)函數(shù)來處理,而是在內(nèi)部自定義get value(){}和set value(){}的方式實現(xiàn)響應(yīng)式,畢竟原始值類型的操作無非就是 讀取 或 設(shè)置,核心還是將 原始值類型 轉(zhuǎn)變?yōu)榱?普通對象類型ref函數(shù)可實現(xiàn)原始值類型轉(zhuǎn)換為 響應(yīng)式數(shù)據(jù),但ref接收的值類型并沒只限定為原始值類型,若接收到的是引用類型,還是會將其通過reactive函數(shù)的方式轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
肝了近 1W 字的內(nèi)容,也是目前寫得字數(shù)最多的文章,人有點麻了 ... 哈哈,希望對大家有所幫助?。?!

關(guān)于本文
作者:熊的貓
https://juejin.cn/post/7147461004954173471

