概覽
vue3中對于普通的代理包含對象和數組兩類,對于數組的方法是重寫了許多方法,具體實現參見packages\reactivity\src\arrayInstrumentations.ts
arrayInstrumentations
實際上就是一個對象,對象的屬性就是數組的方法,屬性值就是重寫的方法。
在BaseReactiveHandler
類的get(target,key,receiver)
方法中,有如下代碼:
get(target, key, receiver) {/**省略 */const targetIsArray = shared.isArray(target);if (!isReadonly2) {let fn;if (targetIsArray && (fn = arrayInstrumentations[key])) {return fn;}if (key === "hasOwnProperty") {return hasOwnProperty;}}/**省略 */
}
可知,當對響應式對象target
進行讀取操作時,會判斷target
是否為數組,且是否key
是否是arrayInstrumentations
中實現的方法,若是,則調用Reflect.get
讀取arrayInstrumentations
中實現的方法并返回。
源碼分析
arrayInstrumentations
中實現的方法有:concat
, entries
, every
, filter
, find
, findIndex
, findLast
, findLastIndex
, forEach
, includes
, indexOf
, join
, lastIndexOf
, map
, pop
, push
, reduce
, reduceRight
, reverse
, shift
, slice
, sort
, splice
, unshift
等
arrayInstrumentations
的實現如下:
const arrayInstrumentations = {__proto__: null,// 可迭代方法[Symbol.iterator]() {return iterator(this, Symbol.iterator, toReactive);},concat(...args) {return reactiveReadArray(this).concat(...args.map((x) => isArray(x) ? reactiveReadArray(x) : x));},entries() {return iterator(this, "entries", (value) => {value[1] = toReactive(value[1]);return value;});},every(fn, thisArg) {return apply(this, "every", fn, thisArg, void 0, arguments);},filter(fn, thisArg) {return apply(this, "filter", fn, thisArg, (v) => v.map(toReactive), arguments);},find(fn, thisArg) {return apply(this, "find", fn, thisArg, toReactive, arguments);},findIndex(fn, thisArg) {return apply(this, "findIndex", fn, thisArg, void 0, arguments);},findLast(fn, thisArg) {return apply(this, "findLast", fn, thisArg, toReactive, arguments);},findLastIndex(fn, thisArg) {return apply(this, "findLastIndex", fn, thisArg, void 0, arguments);},// flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implementforEach(fn, thisArg) {return apply(this, "forEach", fn, thisArg, void 0, arguments);},includes(...args) {return searchProxy(this, "includes", args);},indexOf(...args) {return searchProxy(this, "indexOf", args);},join(separator) {return reactiveReadArray(this).join(separator);},// keys() iterator only reads `length`, no optimisation requiredlastIndexOf(...args) {return searchProxy(this, "lastIndexOf", args);},map(fn, thisArg) {return apply(this, "map", fn, thisArg, void 0, arguments);},pop() {return noTracking(this, "pop");},push(...args) {return noTracking(this, "push", args);},reduce(fn, ...args) {return reduce(this, "reduce", fn, args);},reduceRight(fn, ...args) {return reduce(this, "reduceRight", fn, args);},shift() {return noTracking(this, "shift");},// slice could use ARRAY_ITERATE but also seems to beg for range trackingsome(fn, thisArg) {return apply(this, "some", fn, thisArg, void 0, arguments);},splice(...args) {return noTracking(this, "splice", args);},toReversed() {return reactiveReadArray(this).toReversed();},toSorted(comparer) {return reactiveReadArray(this).toSorted(comparer);},toSpliced(...args) {return reactiveReadArray(this).toSpliced(...args);},unshift(...args) {return noTracking(this, "unshift", args);},values() {return iterator(this, "values", toReactive);}
};
輔助方法
arrayInstrumentations
是一個可迭代對象,實現了Symbol.iterator
方法,在了解arrayInstrumentations
之前,我們先了解下如下幾個個函數:reactiveReadArray
、shallowReadArray
、iterator
、apply
、searchProxy
、noTracking
和reduce
的實現。
reactiveReadArray
reactiveReadArray
用于處理響應式數組的讀取操作,它的實現如下:
function reactiveReadArray(array) {const raw = toRaw(array);if (raw === array) return raw;track(raw, "iterate", ARRAY_ITERATE_KEY);return isShallow(array) ? raw : raw.map(toReactive);
}
若數組是普通數組,則直接返回數組array
,否則調用track
進行依賴收集,iterate
表示進行的是迭代操作的依賴收集,即當使用for...of
、map
、forEach
等迭代方法時建立依賴關系。最后分析,若響應式數組是淺響應,則返回原始數組,否則遍歷原始數組調用toReactive
克隆數組
shallowReadArray
shallowReadArray
用于淺層響應式數組的讀取操作,其實現如下:
function shallowReadArray(arr) {// 先將數組轉為普通數組track(arr = toRaw(arr), "iterate", ARRAY_ITERATE_KEY);return arr;
}
iterator
iterator
用于處理數組的迭代操作,比如數組的values
、entries
等方法,其實現如下:
function iterator(self, method, wrapValue) {// arr 是shallowReadArray返回的普通數組const arr = shallowReadArray(self);const iter = arr[method]();// 當self是深層響應式數組時,需要對迭代器的next方法進行包裝if (arr !== self && !isShallow(self)) {iter._next = iter.next;iter.next = () => {const result = iter._next();if (result.value) {// 對迭代器的next方法進行包裝,當調用next方法時,會調用wrapValue函數對值進行包裝result.value = wrapValue(result.value);}return result;};}return iter;
}
iterator
的三個參數分別表示:self
數組本身、method
迭代方法、wrapValue
包裝值的函數。在調用values
、entries
等方法時會調用它。
apply
apply
用于處理數組的方法調用,比如數組的map
、filter
等方法,其實現如下:
function apply(self, method, fn, thisArg, wrappedRetFn, args) {const arr = shallowReadArray(self);const needsWrap = arr !== self && !isShallow(self);const methodFn = arr[method];if (methodFn !== arrayProto[method]) {const result2 = methodFn.apply(self, args);return needsWrap ? toReactive(result2) : result2;}let wrappedFn = fn;if (arr !== self) {if (needsWrap) {wrappedFn = function(item, index) {return fn.call(this, toReactive(item), index, self);};} else if (fn.length > 2) {wrappedFn = function(item, index) {return fn.call(this, item, index, self);};}}const result = methodFn.call(arr, wrappedFn, thisArg);return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result;
}
apply
方法接收6個參數,分別表示:self
數組本身、method
數組方法、fn
回調函數、thisArg
回調函數的this
值、wrappedRetFn
包裝返回值的函數、args
回調函數的參數數組。
apply
方法的實現邏輯如下:
- 調用
shallowReadArray
方法將數組轉為普通數組 - 判斷是否需要包裝,若數組不是普通數組,并且是深層響應式數組,則需要包裝,記為
needsWrap
- 判斷數組方法
method
是否是數組的原生方法。若不是,則調用自定義方法,并且返回該結果;根據needsWrap
判斷是否需要包裝返回值 - 若數組方法
method
是數組的原生方法,則先判斷數組是否是響應式數組,若是,則調用call
方法執行原生方法arr[method]
,參數為fn
和thisArg
;最后判斷,若needsWrap
為true
且wrappedReFn
存在,則調用wrapped
包裝結果再返回,否則直接返回。 - 若數組方法
method
是數組原生方法,且數組是響應式數組,則需要對傳入的fn
進行封裝一層;若數組是深層響應式數組,即needsWrap
為true
需要包裝,則需要通過toReactive
方法對數組項進行響應式化,否則判斷fn
形參長度大于2,則調用call
方法,傳入this
、item
、index
、self
。 - 最后步驟同*(4)* ,不同的是
wrappedFn
不同。
searchProxy
searchProxy
用于處理數組的includes
、indexOf
、lastIndexOf
方法,其實現如下:
function searchProxy(self, method, args) {const arr = toRaw(self);track(arr, "iterate", ARRAY_ITERATE_KEY);const res = arr[method](...args);if ((res === -1 || res === false) && isProxy(args[0])) {args[0] = toRaw(args[0]);return arr[method](...args);}return res;
}
searchProxy
方法會先調用toRaw
將響應式數組轉為普通數組,然后調用track
收集依賴,然后調用數組的原生方法arr[method](...args)
,若沒找到,則判斷參數是否是代理對象,若是代理對象,則調用toRaw
將代理對象轉為普通對象,最后再次調用數組的原生方法arr[method](...args)
,返回結果。若找到了或者參數不是代理對象,則直接返回res
noTracking
noTracking
的實現如下:
function noTracking(self, method, args = []) {pauseTracking();startBatch();const res = toRaw(self)[method].apply(self, args);endBatch();resetTracking();return res;
}
noTracking
方法的實現邏輯如下:
- 調用
pauseTracking
方法暫停依賴收集 - 調用
startBatch
方法開啟批量更新 - 調用
toRaw
方法將響應式數組轉為普通數組,然后調用數組的原生方法arr[method].apply(self, args)
,返回結果 - 調用
endBatch
方法結束批量更新 - 調用
resetTracking
方法重置依賴收集 - 返回結果
reduce
reduce
方法用于對數組中的元素進行迭代執行fn
,上次的執行結果作為下次執行的參數,返回最后的結果。其實現如下:
function reduce(self, method, fn, args) {const arr = shallowReadArray(self);let wrappedFn = fn;if (arr !== self) {if (!isShallow(self)) {wrappedFn = function(acc, item, index) {return fn.call(this, acc, toReactive(item), index, self);};} else if (fn.length > 3) {wrappedFn = function(acc, item, index) {return fn.call(this, acc, item, index, self);};}}return arr[method](wrappedFn, ...args);
}
對于數組target
先判斷它是不是普通數組,若是,則調用數組的原生方法arr[method](wrappedFn, ...args)
,返回結果;若不是普通數組,則繼續判斷是否是淺層響應式,若不是,則調用fn.call
,傳參時用toReactive
將數據進行響應式處理;若是淺層響應式,則判斷fn
的形參個數是否大于3,若是則包裝下fn
;最后調用arr[method](wrappedFn,...args)
,返回結果。
輔助方法與重寫方法的關聯
輔助方法 | 數組方法 | 功能特點 |
---|---|---|
iterator | ``entries() values() | 創建響應式迭代器 自動深度轉換元素為響應式 |
reactiveReadArray | concat() join() toReversed() toSorted() toSpliced() | 安全讀取數組 處理深度響應式轉換 返回完全響應式的新數組 |
apply | every() filter() find() findIndex() findLast() findLastIndex() forEach() map() some() | 通用函數應用模板 按需深度轉換結果 自動進行依賴收集 可自定義結果處理邏輯 |
searchProxy | includes() indexOf() lastIndexOf() | 安全搜索處理 避免深層代理比較錯誤 收集依賴但不修改數據 |
noTracking | pop() push() shift() splice() unshift() | 繞過響應式系統 直接操作原始數組 不觸發依賴收集 用于突變操作方法 |
reduce | reduce() reduceRight() | 專用降維處理器 特殊處理累計值邏輯 在響應式系統控制下操作 |
重寫的方法
concat(...args)
用于合并數組,返回一個新的響應式數組,不改變原數組。其實現就是先調用reactiveArray
進行依賴收集,再遍歷入參數數組,若數組項是數組,則繼續調用reactiveArray
收集依賴;否則不做處理,最后再調用數組的原生方法concat
合并處理過后的參數數組。
entries
entries
是一個迭代器方法,用于返回數組的鍵值對迭代器,每個迭代項是一個包含鍵和值的數組。vue3對entries
的值進行了包裝
every
every
方法用于判斷數組中的所有元素是否滿足條件,若所有元素都滿足條件,則返回true
,否則返回false
。有一個不滿足就會返回false
。屬于數組的原生方法。
filter
filter
方法用于過濾數組中的元素,返回一個新的數組,新數組中的元素是滿足條件的元素。若數組為空,則返回空數組。也是屬于數組的原生方法。
filter
方法也是基于apply
實現的,不同的是第五個參數wrappedRetFn
會將最后的數組結果轉為響應式。
find
find
方法用于查找數組中的第一個滿足條件的元素,若找到則返回該元素,否則返回undefined
。屬于數組的原生方法。若找到元素,則調用toReactive
方法對元素進行響應式化。
findLast
和find
作用一樣,不過findLast
是從數組末尾開始查找;find
是從數組開頭開始查找。
-
findIndex
、**findLastIndex
**都用用于查找元素索引,因此無需響應式化結果 -
forEach
forEach
方法用于遍歷數組中的每個元素,沒有不返回值。屬于數組的原生方法。
-
includes
、indexOf
、lastIndexOf
用于判斷數組是否包含指定元素,基于searchProxy
方法實現。 -
join
join
方法用于將數組中的所有元素轉換為字符串,返回一個新的字符串。若數組為空,則返回空字符串。屬于數組的原生方法。
vue3中重寫join
方法是基于reactiveReadArray
方法實現,解決了深層響應式數組的join
調用
map
map
方法用于遍歷數組中的每個元素,返回一個新的數組,新數組中的元素是遍歷函數的返回值。若數組為空,則返回空數組。屬于數組的原生方法。
pop
、push
、shift
、unshift
、splice
如上幾個方法會可能改變原數組的長度,為了避免數組的長度變化觸發監聽,因此暫停依賴的收集,它們是基于noTracking
方法實現。
reduce
、reduceRight
這兩個方法就是基于輔助方法reduce
實現的,根據數組的響應式特性決定如何包裝fn
。
reduce
是從數組的開頭進行迭代執行fn
,reduceRight
是從數組的末尾進行迭代執行fn
。
toReversed
、toSorted
和toSpliced
這三個方法都是基于reactiveReadArray
實現,并且它們是ES2023新增的特性,屬于數組的原生方法。
總結
以下是針對 Vue 3 響應式系統中 arrayInstrumentations
對象的完整分析,根據內部使用的核心輔助方法進行分類總結:
方法名稱 | 使用的輔助方法 | 特殊處理 | 響應式行為 |
---|---|---|---|
[Symbol.iterator] | iterator | 使用 toReactive 轉換每個元素 | ? 迭代時自動轉換響應式 |
concat | reactiveReadArray | 遞歸處理嵌套數組 | ? 返回深度響應式的新數組 |
entries | iterator | 轉換值為響應式 value[1] = toReactive(value[1]) | ? 返回鍵值對的響應式迭代器 |
every | apply | 無特殊轉換 | ?? 依賴收集但不修改源數組 |
filter | apply | 結果數組元素使用 v => v.map(toReactive) 轉換 | ? 返回過濾后的響應式新數組 |
find | apply | 找到的元素用 toReactive 轉換 | ?? 單個元素響應式轉換 |
findIndex | apply | 無特殊轉換 | ?? 純數值返回 |
findLast | apply | 找到的元素用 toReactive 轉換 | ?? 單個元素響應式轉換 |
findLastIndex | apply | 無特殊轉換 | ?? 純數值返回 |
forEach | apply | 無特殊轉換 | ?? 僅遍歷不做轉換 |
includes | searchProxy | 自定義搜索處理 | ?? 不觸發深層響應式,但收集依賴 |
indexOf | searchProxy | 自定義搜索處理 | ?? 不觸發深層響應式,但收集依賴 |
join | reactiveReadArray | 直接調用原始方法 | ?? 返回字符串不響應 |
lastIndexOf | searchProxy | 自定義搜索處理 | ?? 不觸發深層響應式,但收集依賴 |
map | apply | 無特殊轉換 | ?? 返回非響應式數組 |
pop | noTracking | 修改操作 | 🛑 禁止依賴收集,直接修改 |
push | noTracking | 修改操作 | 🛑 禁止依賴收集,直接修改 |
reduce | reduce | 專用降維處理 | ?? 收集依賴但不自動轉換值 |
reduceRight | reduce | 專用降維處理 | ?? 收集依賴但不自動轉換值 |
shift | noTracking | 修改操作 | 🛑 禁止依賴收集,直接修改 |
some | apply | 無特殊轉換 | ?? 依賴收集但不修改源數組 |
splice | noTracking | 修改操作 | 🛑 禁止依賴收集,直接修改 |
toReversed | reactiveReadArray | 直接調用 ES2023 新方法 | ? 返回反向的響應式新數組 |
toSorted | reactiveReadArray | 直接調用 ES2023 新方法 | ? 返回排序后的響應式新數組 |
toSpliced | reactiveReadArray | 直接調用 ES2023 新方法 | ? 返回裁剪后的響應式新數組 |
unshift | noTracking | 修改操作 | 🛑 禁止依賴收集,直接修改 |
values | iterator | 使用 toReactive 轉換每個元素 | ? 返回元素的響應式迭代器 |
輔助方法功能說明
輔助方法 | 用途 | 響應式處理特點 |
---|---|---|
iterator | 創建數組迭代器 | ? 自動轉換元素為響應式 (toReactive ) |
reactiveReadArray | 安全讀取數組(見前文分析) | ? 自動處理深層響應式轉換 |
apply | 通用方法應用(在函數調用前后執行依賴跟蹤) | ?? 僅自動收集依賴,不處理結果轉換(除非指定回調) |
searchProxy | 處理數組搜索方法(indexOf/includes 等) | ?? 避免深層代理干擾比較邏輯 |
reduce | 專用降維方法處理器 | ?? 特殊處理累計值邏輯 |
noTracking | 禁止依賴收集的修改操作 | 🛑 完全繞過響應式系統執行原始操作 |