import { isArray } from '@vue/shared' import { TrackOpTypes } from './constants' import { ARRAY_ITERATE_KEY, track } from './dep' import { isProxy, isReactive, isReadonly, isShallow, toRaw, toReactive, toReadonly, } from './reactive' import { endBatch, setActiveSub, startBatch } from './system' /** * Track array iteration and return: * - if input is reactive: a cloned raw array with reactive values * - if input is non-reactive or shallowReactive: the original raw array */ export function reactiveReadArray(array: T[]): T[] { const raw = toRaw(array) if (raw === array) return raw track(raw, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY) return isShallow(array) ? raw : raw.map(toReactive) } /** * Track array iteration and return raw array */ export function shallowReadArray(arr: T[]): T[] { track((arr = toRaw(arr)), TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY) return arr } function toWrapped(target: unknown, item: unknown) { if (isReadonly(target)) { return isReactive(target) ? toReadonly(toReactive(item)) : toReadonly(item) } return toReactive(item) } export const arrayInstrumentations: Record = { __proto__: null, [Symbol.iterator]() { return iterator(this, Symbol.iterator, item => toWrapped(this, item)) }, concat(...args: unknown[]) { return reactiveReadArray(this).concat( ...args.map(x => (isArray(x) ? reactiveReadArray(x) : x)), ) }, entries() { return iterator(this, 'entries', (value: [number, unknown]) => { value[1] = toWrapped(this, value[1]) return value }) }, every( fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { return apply(this, 'every', fn, thisArg, undefined, arguments) }, filter( fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { return apply( this, 'filter', fn, thisArg, v => v.map((item: unknown) => toWrapped(this, item)), arguments, ) }, find( fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { return apply( this, 'find', fn, thisArg, item => toWrapped(this, item), arguments, ) }, findIndex( fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { return apply(this, 'findIndex', fn, thisArg, undefined, arguments) }, findLast( fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { return apply( this, 'findLast', fn, thisArg, item => toWrapped(this, item), arguments, ) }, findLastIndex( fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { return apply(this, 'findLastIndex', fn, thisArg, undefined, arguments) }, // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement forEach( fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { return apply(this, 'forEach', fn, thisArg, undefined, arguments) }, includes(...args: unknown[]) { return searchProxy(this, 'includes', args) }, indexOf(...args: unknown[]) { return searchProxy(this, 'indexOf', args) }, join(separator?: string) { return reactiveReadArray(this).join(separator) }, // keys() iterator only reads `length`, no optimization required lastIndexOf(...args: unknown[]) { return searchProxy(this, 'lastIndexOf', args) }, map( fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { return apply(this, 'map', fn, thisArg, undefined, arguments) }, pop() { return noTracking(this, 'pop') }, push(...args: unknown[]) { return noTracking(this, 'push', args) }, reduce( fn: ( acc: unknown, item: unknown, index: number, array: unknown[], ) => unknown, ...args: unknown[] ) { return reduce(this, 'reduce', fn, args) }, reduceRight( fn: ( acc: unknown, item: unknown, index: number, array: unknown[], ) => unknown, ...args: unknown[] ) { return reduce(this, 'reduceRight', fn, args) }, shift() { return noTracking(this, 'shift') }, // slice could use ARRAY_ITERATE but also seems to beg for range tracking some( fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { return apply(this, 'some', fn, thisArg, undefined, arguments) }, splice(...args: unknown[]) { return noTracking(this, 'splice', args) }, toReversed() { // @ts-expect-error user code may run in es2016+ return reactiveReadArray(this).toReversed() }, toSorted(comparer?: (a: unknown, b: unknown) => number) { // @ts-expect-error user code may run in es2016+ return reactiveReadArray(this).toSorted(comparer) }, toSpliced(...args: unknown[]) { // @ts-expect-error user code may run in es2016+ return (reactiveReadArray(this).toSpliced as any)(...args) }, unshift(...args: unknown[]) { return noTracking(this, 'unshift', args) }, values() { return iterator(this, 'values', item => toWrapped(this, item)) }, } // instrument iterators to take ARRAY_ITERATE dependency function iterator( self: unknown[], method: keyof Array, wrapValue: (value: any) => unknown, ) { // note that taking ARRAY_ITERATE dependency here is not strictly equivalent // to calling iterate on the proxied array. // creating the iterator does not access any array property: // it is only when .next() is called that length and indexes are accessed. // pushed to the extreme, an iterator could be created in one effect scope, // partially iterated in another, then iterated more in yet another. // given that JS iterator can only be read once, this doesn't seem like // a plausible use-case, so this tracking simplification seems ok. const arr = shallowReadArray(self) const iter = (arr[method] as any)() as IterableIterator & { _next: IterableIterator['next'] } if (arr !== self && !isShallow(self)) { iter._next = iter.next iter.next = () => { const result = iter._next() if (!result.done) { result.value = wrapValue(result.value) } return result } } return iter } // in the codebase we enforce es2016, but user code may run in environments // higher than that type ArrayMethods = keyof Array | 'findLast' | 'findLastIndex' const arrayProto = Array.prototype // instrument functions that read (potentially) all items // to take ARRAY_ITERATE dependency function apply( self: unknown[], method: ArrayMethods, fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, wrappedRetFn?: (result: any) => unknown, args?: IArguments, ) { const arr = shallowReadArray(self) const needsWrap = arr !== self && !isShallow(self) // @ts-expect-error our code is limited to es2016 but user code is not const methodFn = arr[method] // #11759 // If the method being called is from a user-extended Array, the arguments will be unknown // (unknown order and unknown parameter types). In this case, we skip the shallowReadArray // handling and directly call apply with self. if (methodFn !== arrayProto[method as any]) { const result = methodFn.apply(self, args) return needsWrap ? toReactive(result) : result } let wrappedFn = fn if (arr !== self) { if (needsWrap) { wrappedFn = function (this: unknown, item, index) { return fn.call(this, toWrapped(self, item), index, self) } } else if (fn.length > 2) { wrappedFn = function (this: unknown, item, index) { return fn.call(this, item, index, self) } } } const result = methodFn.call(arr, wrappedFn, thisArg) return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result } // instrument reduce and reduceRight to take ARRAY_ITERATE dependency function reduce( self: unknown[], method: keyof Array, fn: (acc: unknown, item: unknown, index: number, array: unknown[]) => unknown, args: unknown[], ) { const arr = shallowReadArray(self) let wrappedFn = fn if (arr !== self) { if (!isShallow(self)) { wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, toWrapped(self, item), index, self) } } else if (fn.length > 3) { wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, item, index, self) } } } return (arr[method] as any)(wrappedFn, ...args) } // instrument identity-sensitive methods to account for reactive proxies function searchProxy( self: unknown[], method: keyof Array, args: unknown[], ) { const arr = toRaw(self) as any track(arr, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY) // we run the method using the original args first (which may be reactive) const res = arr[method](...args) // if that didn't work, run it again using raw values. if ((res === -1 || res === false) && isProxy(args[0])) { args[0] = toRaw(args[0]) return arr[method](...args) } return res } // instrument length-altering mutation methods to avoid length being tracked // which leads to infinite loops in some cases (#2137) function noTracking( self: unknown[], method: keyof Array, args: unknown[] = [], ) { startBatch() const prevSub = setActiveSub() const res = (toRaw(self) as any)[method].apply(self, args) setActiveSub(prevSub) endBatch() return res }