聽說你很瞭解 Vue3 響應式?
前言
【A】:能不能說說 Vue3 響應式都處理了哪些數據類型?都怎麼處理的呀?
【B】:能,只能說一點點...
【A】:...
只要問到 Vue
相關的內容,似乎總繞不過 響應式原理 的話題,隨之而來的回答必然是圍繞着 Object.defineProperty
和 Proxy
來展開(即 Vue2
和 Vue3
),但若繼續追問某些具體實現是不是就倉促結束回答了(你跑我追,你不跑我還追 )。
本文就不再過多介紹 Vue2
中響應式的處理,但是會有簡單提及,下面就來看看 Vue3
中是如何處理 原始值、Object、Array、Set、Map 等數據類型的響應式。
從 Object.defineProperty
到 Proxy
一切的一切還得從 Object.defineProperty
開始講起,那是一個不一樣的 API
... (bgm 響起,自行體會)
Object.defineProperty
Object.defineProperty(obj, prop, descriptor)
方法會直接在一個對象上定義一個 新屬性,或修改一個 對象 的 現有屬性,並返回此對象,其參數具體爲:
-
obj
:要定義屬性的對象 -
prop
:要定義或修改的 屬性名稱 或Symbol
-
descriptor
:要定義或修改的 屬性描述符
從以上的描述就可以看出一些限制,比如:
-
目標是 對象屬性,不是 整個對象
-
一次只能 定義或修改一個屬性
-
當然有對應的一次處理多個屬性的方法
Object.defineProperties()
,但在vue
中並不適用,因爲vue
不能提前知道用戶傳入的對象都有什麼屬性,因此還是得經過類似Object.keys() + for
循環的方式獲取所有的key -> value
,而這其實是沒有必要使用Object.defineProperties()
在 Vue2 中的缺陷
Object.defineProperty()
實際是通過 定義 或 修改 對象屬性
的描述符來實現 數據劫持,其對應的缺點也是沒法被忽略的:
-
只能攔截對象屬性的
get
和set
操作,比如無法攔截delete
、in
、方法調用
等操作 -
動態添加新屬性(響應式丟失)
-
保證後續使用的屬性要在初始化聲明
data
時進行定義 -
使用
this.$set()
設置新屬性 -
通過
delete
刪除屬性(響應式丟失) -
使用
this.$delete()
刪除屬性 -
使用數組索引 替換 / 新增 元素(響應式丟失)
-
使用
this.$set()
設置新元素 -
使用數組
push、pop、shift、unshift、splice、sort、reverse
等 原生方法 改變原數組時(響應式丟失) -
使用 重寫 / 增強 後的
push、pop、shift、unshift、splice、sort、reverse
方法 -
一次只能對一個屬性實現 數據劫持,需要遍歷對所有屬性進行劫持
-
數據結構複雜時(屬性值爲 引用類型數據),需要通過 遞歸 進行處理
【擴展】Object.defineProperty
和 Array
?
它們有啥關係,其實沒有啥關係,只是大家習慣性的會回答 Object.defineProperty
不能攔截 Array
的操作,這句話說得對但也不對。
使用 Object.defineProperty 攔截 Array
Object.defineProperty
可用於實現對象屬性的 get
和 set
攔截,而數組其實也是對象,那自然是可以實現對應的攔截操作,如下:
Vue2 爲什麼不使用 Object.defineProperty 攔截 Array?
尤大在曾在 GitHub
的 Issue
中做過如下回復:
說實話性能問題到底指的是什麼呢?下面是總結了一些目前看到過的回答:
-
數組 和 普通對象 在使用場景下有區別,在項目中使用數組的目的大多是爲了 遍歷,即比較少會使用
array[index] = xxx
的形式,更多的是使用數組的Api
的方式 -
數組長度是多變的,不可能像普通對象一樣先在
data
選項中提前聲明好所有元素,比如通過array[index] = xxx
方式賦值時,一旦index
的值超過了現有的最大索引值,那麼當前的添加的新元素也不會具有響應式 -
數組存儲的元素比較多,不可能爲每個數組元素都設置
getter/setter
-
無法攔截數組原生方法如
push、pop、shift、unshift
等的調用,最終仍需 重寫 / 增強 原生方法
Proxy & Reflect
由於在 Vue2
中使用 Object.defineProperty
帶來的缺陷,導致在 Vue2
中不得不提供了一些額外的方法(如:Vue.set、Vue.delete()
)解決問題,而在 Vue3
中使用了 Proxy
的方式來實現 數據劫持,而上述的問題在 Proxy
中都可以得到解決。
Proxy
Proxy
主要用於創建一個 對象的代理,從而實現基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數調用等),本質上是通過攔截對象 內部方法 的執行實現代理,而對象本身根據規範定義的不同又會區分爲 常規對象 和 異質對象(這不是重點,可自行了解)。
-
new Proxy(target, handler)
是針對整個對象進行的代理,不是某個屬性 -
代理對象屬性擁有 讀取、修改、刪除、新增、是否存在屬性 等操作相應的捕捉器,
-
get()
屬性 讀取 操作的捕捉器 -
set()
屬性 設置 操作的捕捉器 -
deleteProperty()
是delete
操作符的捕捉器 -
ownKeys()
是Object.getOwnPropertyNames
方法和Object.getOwnPropertySymbols
方法的捕捉器 -
has()
是in
操作符的捕捉器
Reflect
Reflect
是一個內置的對象,它提供攔截 JavaScript
操作的方法,這些方法與 Proxy handlers
提供的的方法是一一對應的,且 Reflect
不是一個函數對象,即不能進行實例化,其所有屬性和方法都是靜態的。
-
Reflect.get(target, propertyKey[, receiver])
獲取對象身上某個屬性的值,類似於target[name]
-
Reflect.set(target, propertyKey, value[, receiver])
將值分配給屬性的函數。返回一個Boolean
,如果更新成功,則返回true
-
Reflect.deleteProperty(target, propertyKey)
作爲函數的delete
操作符,相當於執行delete target[name]
-
Reflect.ownKeys(target)
返回一個包含所有自身屬性(不包含繼承屬性)的數組。(類似於Object.keys()
, 但不會受enumerable
影響) -
Reflect.has(target, propertyKey)
判斷一個對象是否存在某個屬性,和in
運算符 的功能完全相同
Proxy 爲什麼需要 Reflect 呢?
在 Proxy
的 get(target, key, receiver)、set(target, key, newVal, receiver)
的捕獲器中都能接到前面所列舉的參數:
-
target
指的是 原始數據對象 -
key
指的是當前操作的 屬性名 -
newVal
指的是當前操作接收到的 最新值 -
receiver
指向的是當前操作 正確的上下文
怎麼理解 Proxy handler
中 receiver
指向的是當前操作正確上的下文呢?
-
正常情況下,
receiver
指向的是 當前的代理對象 -
特殊情況下,
receiver
指向的是 引發當前操作的對象 -
通過
Object.setPrototypeOf()
方法將代理對象proxy
設置爲普通對象obj
的原型 -
通過
obj.name
訪問其不存在的name
屬性,由於原型鏈的存在,最終會訪問到proxy.name
上,即觸發get
捕獲器
在 Reflect
的方法中通常只需要傳遞 target、key、newVal
等,但爲了能夠處理上述提到的特殊情況,一般也需要傳遞 receiver
參數,因爲 Reflect 方法中傳遞的 receiver 參數代表執行原始操作時的 this
指向,比如:Reflect.get(target, key , receiver)
、Reflect.set(target, key, newVal, receiver)
。
總結:Reflect
是爲了在執行對應的攔截操作的方法時能 傳遞正確的 this
上下文。
Vue3 如何使用 Proxy 實現數據劫持?
Vue3
中提供了 reactive()
和 ref()
兩個方法用來將 目標數據 變成 響應式數據,而通過 Proxy
來實現 數據劫持(或代理) 的具體實現就在其中,下面一起來看看吧!
reactive 函數
從源碼來看,其核心其實就是 createReactiveObject(...)
函數,那麼繼續往下查看對應的內容
源碼位置:packages\reactivity\src\reactive.ts
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 若目標對象是響應式的只讀數據,則直接返回
if (isReadonly(target)) {
return target
}
// 否則將目標數據嘗試變成響應式數據
return createReactiveObject(
target,
false,
mutableHandlers, // 對象類型的 handlers
mutableCollectionHandlers, // 集合類型的 handlers
reactiveMap
)
}
createReactiveObject() 函數
源碼的體現也是非常簡單,無非就是做一些前置判斷處理:
-
若目標數據是 原始值類型,直接向返回 原數據
-
若目標數據的
__v_raw
屬性爲true
,且是【非響應式數據】或 不是通過調用readonly()
方法,則直接返回 原數據 -
若目標數據已存在相應的
proxy
代理對象,則直接返回 對應的代理對象 -
若目標數據不存在對應的 白名單數據類型 中,則直接返回原數據,支持響應式的數據類型如下:
-
可擴展的對象,即是否可以在它上面添加新的屬性
-
__v_skip 屬性不存在或值爲 false 的對象
-
數據類型爲
Object、Array、Map、Set、WeakMap、WeakSet
的對象 -
其他數據都統一被認爲是 無效的響應式數據對象
-
通過
Proxy
創建代理對象,根據目標數據類型選擇不同的Proxy handlers
看來具體的實現又在不同數據類型的 捕獲器 中,即下面源碼的 collectionHandlers
和 baseHandlers
,而它們則對應的是在上述 reactive()
函數中爲 createReactiveObject()
函數傳遞的 mutableCollectionHandlers
和 mutableHandlers
參數。
源碼位置: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
}
// 目標數據的 __v_raw 屬性若爲 true,且是【非響應式數據】或 不是通過調用 readonly() 方法,則直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 目標對象已存在相應的 proxy 代理對象,則直接返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有在白名單中的值類型纔可以被代理監測,否則直接返回
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 創建代理對象
const proxy = new Proxy(
target,
// 若目標對象是集合類型(Set、Map)則使用集合類型對應的捕獲器,否則使用基礎捕獲器
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 將對應的代理對象存儲在 proxyMap 中
proxyMap.set(target, proxy)
return proxy
}
捕獲器 Handlers
對象類型的捕獲器 — mutableHandlers
這裏的對象類型指的是 數組 和 普通對象
源碼位置:packages\reactivity\src\baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
以上這些捕獲器其實就是我們在上述 Proxy
部分列舉出來的捕獲器,顯然可以攔截對普通對象的如下操作:
-
讀取,如
obj.name
-
設置,如
obj.name = 'zs'
-
刪除屬性,如
delete obj.name
-
判斷是否存在對應屬性,如
name in obj
-
獲取對象自身的屬性值,如
obj.getOwnPropertyNames()
和obj.getOwnPropertySymbols()
get
捕獲器
具體信息在下面的註釋中,這裏只列舉核心內容:
-
若當前數據對象是 數組,則 重寫 / 增強 數組對應的方法
-
數組元素的 查找方法:
includes、indexOf、lastIndexOf
-
修改原數組 的方法:
push、pop、unshift、shift、splice
-
若當前數據對象是 普通對象 ,且非 只讀 的則通過
track(target, TrackOpTypes.GET, key)
進行 依賴收集 -
若當前數據對象是 淺層響應 的,則直接返回其對應屬性值
-
若當前數據對象是 ref 類型的,則會進行 自動脫 ref
-
若當前數據對象的屬性值是 對象類型
-
若當前屬性值屬於 只讀的,則通過
readonly(res)
向外返回其結果 -
否則會將當前屬性值以
reactive(res)
向外返回 proxy 代理對象 -
否則直接向外返回對應的 屬性值
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 當直接通過指定 key 訪問 vue 內置自定義的對象屬性時,返回其對應的值
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
}
// 判斷是否爲數組類型
const targetIsArray = isArray(target)
// 數組對象
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 重寫/增強數組的方法:
// - 查找方法:includes、indexOf、lastIndexOf
// - 修改原數組的方法:push、pop、unshift、shift、splice
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 獲取對應屬性值
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 依賴收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 淺層響應
if (shallow) {
return res
}
// 若是 ref 類型響應式數據,會進行【自動脫 ref】,但不支持【數組】+【索引】的訪問方式
if (isRef(res)) {
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
// 屬性值是對象類型:
// - 是隻讀屬性,則通過 readonly() 返回結果,
// - 且是非只讀屬性,則遞歸調用 reactive 向外返回 proxy 代理對象
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
set
捕獲器
除去額外的邊界處理,其實核心還是 更新屬性值,並通過 trigger(...)
觸發依賴更新
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 保存舊的數據
let oldValue = (target as any)[key]
// 若原數據值屬於 只讀 且 ref 類型,並且新數據值不屬於 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
}
// 是否存在對應的 key
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 設置對應值
const result = Reflect.set(target, key, value, receiver)
// 若目標對象是原始原型鏈上的內容(非自定義添加),則不觸發依賴更新
if (target === toRaw(receiver)) {
if (!hadKey) {
// 目標對象不存在對應的 key,則爲新增操作
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 目標對象存在對應的值,則爲修改操作
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
// 返回修改結果
return result
}
}
deleteProperty
& has
& ownKeys
捕獲器
這三個捕獲器內容非常簡潔,其中 has
和 ownKeys
本質也屬於 讀取操作,因此需要通過 track()
進行依賴收集,而 deleteProperty
相當於修改操作,因此需要 trigger()
觸發更新
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)
// 目標對象上存在對應的 key ,並且能成功刪除,纔會觸發依賴更新
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)
}
數組類型捕獲器 —— arrayInstrumentations
數組類型 和 對象類型 的大部分操作是可以共用的,比如 obj.name
和 arr[index]
等,但數組類型的操作還是會比對象類型更豐富一些,而這些就需要特殊處理。
源碼位置:
packages\reactivity\src\collectionHandlers.ts
處理數組索引 index
和 length
數組的 index
和 length
是會相互影響的,比如存在數組 const arr = [1]
:
-
arr[1] = 2
的操作會隱式修改length
的屬性值 -
arr.length = 0
的操作會導致原索引位的值發生變更
爲了能夠合理觸發和 length
相關副作用函數的執行,在 set()
捕獲器中會判斷當前操作的類型:
-
當
Number(key) < target.length
證明是修改操作,對應TriggerOpTypes.SET
類型,即當前操作不會改變length
的值,不需要 觸發和length
相關副作用函數的執行 -
當
Number(key) >= target.length
證明是新增操作,TriggerOpTypes.ADD
類型,即當前操作會改變length
的值,需要 觸發和length
相關副作用函數的執行
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
}
}
處理數組的查找方法
數組的查找方法包括 includes
、indexOf
、lastIndexOf
,這些方法通常情況下是能夠按預期進行工作,但還是需要對某些特殊情況進行處理:
-
當查找的目標數據是響應式數據本身時,得到的就不是預期結果
const obj = {} const proxy = reactive([obj]) console.log(proxy.includs(proxy[0])) // false
-
【產生原因】首先這裏涉及到了兩次讀取操作,第一次 是
proxy[0]
此時會觸發get
捕獲器併爲obj
生成對應代理對象並返回,第二次 是proxy.includs()
的調用,它會遍歷數組的每個元素,即會觸發get
捕獲器,並又生成一個新的代理對象並返回,而這兩次生成的代理對象不是同一個,因此返回false
-
【解決方案】源碼中會在
get
中設置一個名爲proxyMap
的WeakMap
集合用於存儲每個響應式對象,在觸發get
時優先返回proxyMap
存在的響應式對象,這樣不管觸發多少次get
都能返回相同的響應式數據 -
當在響應式對象中查找原始數據時,得到的就不是預期結果
const obj = {} const proxy = reactive([obj]) console.log(proxy.includs(obj)) // false
-
【產生原因】
proxy.includes()
會觸發get
捕獲器併爲obj
生成對應代理對象並返回,而includes
方法的參數傳遞的是 原始數據,相當於此時是 響應式對象 和 原始數據對象 進行比較,因此對應的結果一定是爲false
-
【解決方案】核心就是將它們的數據類型統一,即統一都使用 原始值數據對比 或 響應式數據對比 ,由於
includes()
的方法本身並不支持對傳入參數或內部響應式數據的處理,因此需要自定義以上對應的數組查找方法 -
在 重寫 / 增強 的
includes
、indexOf
、lastIndexOf
等方法中,會將當前方法內部訪問到的響應式數據轉換爲原始數據,然後調用數組對應的原始方法進行查找,若查找結果爲true
則直接返回結果 -
若以上操作沒有查找到,則通過將當前方法傳入的參數轉換爲原始數據,在調用數組的原始方法,此時直接將對應的結果向外進行返回
源碼位置:packages\reactivity\src\baseHandlers.ts
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 外部調用上述方法,默認其內的 this 指向的是代理數組對象,
// 但實際上是需要通過原始數組中進行遍歷查找
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
}
}
})
處理數組影響 length
的方法
隱式修改數組長度的原型方法包括 push
、pop
、shift
、unshift
、splice
等,在調用這些方法的同時會間接的讀取數組的 length
屬性,又因爲這些方法具有修改數組長度的能力,即相當於 length
的設置操作,若不進行特殊處理,會導致與 length
屬性相關的副作用函數被重複執行,即 棧溢出,比如:
const proxy = reactive([])
// 第一個副作用函數
effect(() => {
proxy.push(1) // 讀取 + 設置 操作
})
// 第二個副作用函數
effect(() => {
proxy.push(2) // 讀取 + 設置 操作(此時進行 trigger 時,會觸發包括第一個副作用函數的內容,然後循環導致棧溢出)
})
在源碼中還是通過 重寫 / 增強 上述對應數組方法的形式實現自定義的邏輯處理:
-
在調用真正的數組原型方法前,會通過設置
pauseTracking()
方法來禁止track
依賴收集 -
在調用數組原生方法後,在通過
resetTracking()
方法恢復track
進行依賴收集 -
實際上以上的兩個方法就是通過控制
shouldTrack
變量爲true
或false
,使得在track
函數執行時是否要執行原來的依賴收集邏輯
源碼位置: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
}
})
集合類型的捕獲器 — mutableCollectionHandlers
集合類型 包括 Map
、WeakMap
、Set
、WeakSet
等,而對 集合類型 的 代理模式 和 對象類型 需要有所不同,因爲 集合類型 和 對象類型 的操作方法是不同的,比如:
Map
類型 的原型 屬性 和 方法 如下:
-
size
-
clear()
-
delete(key)
-
has(key)
-
get(key)
-
set(key)
-
keys()
-
values()
-
entries()
-
forEach(cb)
Set
類型 的原型 屬性 和 方法 如下:
-
size
-
add(value)
-
clear()
-
delete(value)
-
has(value)
-
keys()
-
values()
-
entries()
-
forEach(cb)
源碼位置:
packages\reactivity\src\collectionHandlers.ts
解決 代理對象
無法訪問 集合類型
對應的 屬性
和 方法
代理集合類型的第一個問題,就是代理對象沒法獲取到集合類型的屬性和方法,比如:
從報錯信息可以看出 size
屬性是一個訪問器屬性,所以它被作爲方法調用了,而主要錯誤原因就是在這個訪問器中的 this
指向的是 代理對象,在源碼中就是通過爲這些特定的 屬性 和 方法 定義對應的 key 的 mutableInstrumentations 對象,並且在其對應的 屬性 和 方法 中將 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)
}
省略其他代碼
處理集合類型的響應式
集合建立響應式核心還是 track
和 trigger
,轉而思考的問題就變成,什麼時候需要 track
、什麼時候需要 trigger
:
-
track
時機:get()、get size()、has()、forEach()
-
trigger
時機:add()、set()、delete()、clear()
這裏涉及一些優化的內容,比如:
-
在
add()
中通過has()
判斷當前添加的元素是否已經存在於Set
集合中時,若已存在就不需要進行trigger()
操作,因爲Set
集合本身的一個特性就是 去重 -
在
delete()
中通過has()
判斷當前刪除的元素或屬性是否存在,若不存在就不需要進行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
}
省略其他代碼
}
避免污染原始數據
通過重寫集合類型的方法並手動指定其中的 this
指向爲 原始對象 的方式,解決 代理對象 無法訪問 集合類型 對應的 屬性 和 方法 的問題,但這樣的實現方式也帶來了另一個問題:原始數據被污染
。
簡單來說,我們只希望 代理對象(響應式對象
) 才具備 依賴收集 (track
) 和 依賴更新 (trigger
) 的能力,而通過 原始數據 進行的操作不應該具有響應式的能力。
如果只是單純的把所有操作直接作用到 原始對象 上就不能保證這個結果,比如:
// 原數數據 originalData1
const originalData1 = new Map({});
// 代理對象 proxyData1
const proxyData1 = reactive(originalData1);
// 另一個代理對象 proxyData2
const proxyData2 = reactive(new Map({}));
// 將 proxyData2 做爲 proxyData1 一個鍵值
// 【注意】此時的 set() 經過重寫,其內部 this 已經指向 原始對象(originalData1),等價於 原始對象 originalData1 上存儲了一個 響應式對象 proxyData2
proxyData1.set("proxyData2", proxyData2);
// 若不做額外處理,如下基於 原始數據的操作 就會觸發 track 和 trigger
originalData1.get("proxyData2").set("name", "zs");
在源碼中的解決方案也是很簡單,直接通過 value = toRaw(value)
獲取當前設置值對應的 原始數據,這樣舊可以避免 響應式數據對原始數據的污染。
處理 forEach
回調參數
首先 Map.prototype.forEach(callbackFn [, thisArg\])
其中 callbackFn
回調函數會接收三個參數:
-
當前的 值
value
-
當前的 鍵
key
-
正在被遍歷的
Map
對象(原始對象)
遍歷操作 等價於 讀取操作,在處理 普通對象 的 get()
捕獲器中有一個處理,如果當前訪問的屬性值是 對象類型 那麼就會向外返回其對應的 代理對象,目的是實現 惰性響應 和 深層響應,這個處理也同樣適用於 集合類型。
因此,在源碼中通過 callback.call(thisArg, wrap(value), wrap(key), observed)
的方式將 Map
類型的 鍵 和 值 進行響應式處理,以及進行 track
操作,因爲 Map
類型關注的就是 鍵 和 值。
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)
})
}
處理迭代器
集合類型的迭代器方法:
-
entries()
-
keys()
-
values()
Map
和 Set
都實現了 可迭代協議(即 Symbol.iterator
方法,而 迭代器協議 是指 一個對象實現了 next
方法),因此它們還可以通過 for...of
的方式進行遍歷。
根據對 forEach
的處理,不難知道涉及遍歷的方法,終究還是得將其對應的遍歷的 鍵、值 進行響應式包裹的處理,以及進行 track
操作,而原本的的迭代器方法沒辦法實現,因此需要內部自定義迭代器協議。
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
省略其他代碼
})
這一部分的源碼涉及的內容比較多,以上只是簡單的總結一下,更詳細的內容可查看對應的源碼內容。
ref 函數 — 原始值的響應式
原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined、null
等類型的值,我們知道用 Object.defineProperty
肯定是不支持,因爲它攔截的就是對象屬性的操作,都說 Proxy
比 Object.defineProperty
強,那麼它能不能直接支持呢?
直接支持是肯定不能的,別忘了 Proxy
代理的目標也還是對象類型呀,它的強是在自己的所屬領域,跨領域也是遭不住的。
因此在 Vue3
的 ref
函數中對原始值的處理方式是通過爲 原始值類型 提供一個通過 new RefImpl(rawValue, shallow)
實例化得到的 包裹對象,說白了還是將原始值類型變成對象類型,但 ref
函數的參數並 不限制數據類型:
-
原始值類型,
ref
函數中會爲原始值類型數據創建RefImpl
實例對象(必須通過.value
的方式訪問數據),並且實現自定義的get、set
用於分別進行 依賴收集 和 依賴更新,注意的是這裏並不會通過Proxy
爲原始值類型創建代理對象,準確的說在RefImpl
內部自定義實現的get、set
就實現了對原始值類型的攔截操作,因爲原始值類型不需要向對象類型設置那麼多的捕獲器
-
對象類型,
ref
函數中除了爲 對象類型數據創建RefImpl
實例對象之外,還會通過reactive
函數將其轉換爲響應式數據,其實主要還是爲了支持類似如下的操作const refProxy = ref({name: 'zs'}) refProxy.value.name = 'ls'
-
依賴容器 dep,在
ref
類型中依賴存儲的位置就是每個ref
實例對象上的dep
屬性,它本質就是一個Set
實例,觸發get
時往dep
中添加副作用函數(依賴),觸發set
時從dep
中依次取出副作用函數執行
源碼位置: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) {
// 獲取原始數據
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 中取出對應的 effect 函數依次遍歷執行
triggerRefValue(this, newVal)
}
}
}
// 若當前 value 是 對象類型,纔會通過 reactive 轉換爲響應式數據
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
Vue3 如何進行依賴收集?
在 Vue2
中依賴的收集方式是通過 Dep
和 Watcher
的 觀察者模式 來實現的,是不是還能想起初次瞭解 Dep
和 Watcher
之間的這種 剪不斷理還亂
的關係時的心情 ......
依賴收集 其實說的就是 track
函數需要處理的內容:
-
聲明
targetMap
作爲一個容器,用於保存和當前響應式對象相關的依賴內容,本身是一個WeakMap
類型 -
選擇
WeakMap
類型作爲容器,是因爲WeakMap
對 鍵(對象類型)的引用是 弱類型 的,一旦外部沒有對該 鍵(對象類型)保持引用時,WeakMap
就會自動將其刪除,即 能夠保證該對象能夠正常被垃圾回收 -
而
Map
類型對 鍵 的引用則是 強引用 ,即便外部沒有對該對象保持引用,但至少還存在Map
本身對該對象的引用關係,因此會導致該對象不能及時的被垃圾回收 -
將對應的 響應式數據對象 作爲
targetMap
的 鍵,存儲和當前響應式數據對象相關的依賴關係depsMap
(屬於Map
實例),即depsMap
存儲的就是和當前響應式對象的每一個key
對應的具體依賴 -
將
deps
(屬於Set
實例)作爲depsMap
每個key
對應的依賴集合,因爲每個響應式數據可能在多個副作用函數中被使用,並且Set
類型用於自動去重的能力
可視化結構如下:
源碼位置:packages\reactivity\src\effect.ts
const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 當前應該進行依賴收集 且 有對應的副作用函數時,纔會進行依賴收集
if (shouldTrack && activeEffect) {
// 從容器中取出【對應響應式數據對象】的依賴關係
let depsMap = targetMap.get(target)
if (!depsMap) {
// 若不存在,則進行初始化
targetMap.set(target, (depsMap = new Map()))
}
// 獲取和對【應響應式數據對象 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
}
最後
以上就是針對 Vue3
中對不同數據類型的處理的內容,無論是 Vue2
還是 Vue3
響應式的核心都是 數據劫持 / 代理、依賴收集、依賴更新,只不過由於實現數據劫持方式的差異從而導致具體實現的差異,在 Vue3
中值得注意的是:
-
普通對象類型 可以直接配合
Proxy
提供的捕獲器實現響應式 -
數組類型 也可以直接複用大部分和 普通對象類型 的捕獲器,但其對應的查找方法和隱式修改
length
的方法仍然需要被 重寫 / 增強 -
爲了支持 集合類型 的響應式,也對其對應的方法進行了 重寫 / 增強
-
原始值數據類型主要通過
ref
函數來進行響應式處理,不過內容不會對 原始值類型 使用reactive(或 Proxy)
函數來處理,而是在內部自定義get value(){}
和set value(){}
的方式實現響應式,畢竟原始值類型的操作無非就是 讀取 或 設置,核心還是將 原始值類型 轉變爲了 普通對象類型 -
ref
函數可實現原始值類型轉換爲 響應式數據,但ref
接收的值類型並沒只限定爲原始值類型,若接收到的是引用類型,還是會將其通過reactive
函數的方式轉換爲響應式數據
作者:熊的貓
https://juejin.cn/post/7147461004954173471
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2MIrf5fJW2rXHHC6xt_tyA