vue3源码reactivity响应式之数组代理的方法
概览
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 | 禁止依赖收集的修改操作 | 🛑 完全绕过响应式系统执行原始操作 |