2024-02-26 18:25:52 +08:00
|
|
|
import { TrackOpTypes } from './constants'
|
|
|
|
import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
|
|
|
|
import { isProxy, isShallow, toRaw, toReactive } from './reactive'
|
|
|
|
import { ARRAY_ITERATE_KEY, track } from './dep'
|
2024-09-04 20:21:10 +08:00
|
|
|
import { isArray } from '@vue/shared'
|
2024-02-26 18:25:52 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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<T>(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<T>(arr: T[]): T[] {
|
|
|
|
track((arr = toRaw(arr)), TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)
|
|
|
|
return arr
|
|
|
|
}
|
|
|
|
|
|
|
|
export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
|
|
|
|
__proto__: null,
|
|
|
|
|
|
|
|
[Symbol.iterator]() {
|
|
|
|
return iterator(this, Symbol.iterator, toReactive)
|
|
|
|
},
|
|
|
|
|
2024-09-04 20:21:10 +08:00
|
|
|
concat(...args: unknown[]) {
|
2024-02-26 18:25:52 +08:00
|
|
|
return reactiveReadArray(this).concat(
|
2024-09-04 20:21:10 +08:00
|
|
|
...args.map(x => (isArray(x) ? reactiveReadArray(x) : x)),
|
2024-02-26 18:25:52 +08:00
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
entries() {
|
|
|
|
return iterator(this, 'entries', (value: [number, unknown]) => {
|
|
|
|
value[1] = toReactive(value[1])
|
|
|
|
return value
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
every(
|
|
|
|
fn: (item: unknown, index: number, array: unknown[]) => unknown,
|
|
|
|
thisArg?: unknown,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'every', fn, thisArg, undefined, arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
filter(
|
|
|
|
fn: (item: unknown, index: number, array: unknown[]) => unknown,
|
|
|
|
thisArg?: unknown,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'filter', fn, thisArg, v => v.map(toReactive), arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
find(
|
|
|
|
fn: (item: unknown, index: number, array: unknown[]) => boolean,
|
|
|
|
thisArg?: unknown,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'find', fn, thisArg, toReactive, arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
findIndex(
|
|
|
|
fn: (item: unknown, index: number, array: unknown[]) => boolean,
|
|
|
|
thisArg?: unknown,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'findIndex', fn, thisArg, undefined, arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
findLast(
|
|
|
|
fn: (item: unknown, index: number, array: unknown[]) => boolean,
|
|
|
|
thisArg?: unknown,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'findLast', fn, thisArg, toReactive, arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
findLastIndex(
|
|
|
|
fn: (item: unknown, index: number, array: unknown[]) => boolean,
|
|
|
|
thisArg?: unknown,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'findLastIndex', fn, thisArg, undefined, arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'forEach', fn, thisArg, undefined, arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
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 optimisation required
|
|
|
|
|
|
|
|
lastIndexOf(...args: unknown[]) {
|
|
|
|
return searchProxy(this, 'lastIndexOf', args)
|
|
|
|
},
|
|
|
|
|
|
|
|
map(
|
|
|
|
fn: (item: unknown, index: number, array: unknown[]) => unknown,
|
|
|
|
thisArg?: unknown,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'map', fn, thisArg, undefined, arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
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,
|
|
|
|
) {
|
2024-08-10 15:54:38 +08:00
|
|
|
return apply(this, 'some', fn, thisArg, undefined, arguments)
|
2024-02-26 18:25:52 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
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', toReactive)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// instrument iterators to take ARRAY_ITERATE dependency
|
|
|
|
function iterator(
|
|
|
|
self: unknown[],
|
2024-06-26 08:43:24 +08:00
|
|
|
method: keyof Array<unknown>,
|
2024-02-26 18:25:52 +08:00
|
|
|
wrapValue: (value: any) => unknown,
|
|
|
|
) {
|
|
|
|
// note that taking ARRAY_ITERATE dependency here is not strictly equivalent
|
|
|
|
// to calling iterate on the proxified 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)
|
2024-06-26 08:43:24 +08:00
|
|
|
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
|
|
|
|
_next: IterableIterator<unknown>['next']
|
|
|
|
}
|
2024-02-26 18:25:52 +08:00
|
|
|
if (arr !== self && !isShallow(self)) {
|
2024-06-26 08:43:24 +08:00
|
|
|
iter._next = iter.next
|
2024-02-26 18:25:52 +08:00
|
|
|
iter.next = () => {
|
2024-06-26 08:43:24 +08:00
|
|
|
const result = iter._next()
|
2024-02-26 18:25:52 +08:00
|
|
|
if (result.value) {
|
|
|
|
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<any> | 'findLast' | 'findLastIndex'
|
|
|
|
|
2024-08-10 15:54:38 +08:00
|
|
|
const arrayProto = Array.prototype
|
2024-02-26 18:25:52 +08:00
|
|
|
// 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,
|
2024-07-29 09:22:18 +08:00
|
|
|
wrappedRetFn?: (result: any) => unknown,
|
2024-08-10 15:54:38 +08:00
|
|
|
args?: IArguments,
|
2024-02-26 18:25:52 +08:00
|
|
|
) {
|
|
|
|
const arr = shallowReadArray(self)
|
2024-08-16 16:23:09 +08:00
|
|
|
const needsWrap = arr !== self && !isShallow(self)
|
2024-08-10 15:54:38 +08:00
|
|
|
// @ts-expect-error our code is limited to es2016 but user code is not
|
2024-08-16 16:23:09 +08:00
|
|
|
const methodFn = arr[method]
|
2024-09-03 17:32:13 +08:00
|
|
|
|
|
|
|
// #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)
|
2024-08-16 16:23:09 +08:00
|
|
|
return needsWrap ? toReactive(result) : result
|
2024-08-10 15:54:38 +08:00
|
|
|
}
|
|
|
|
|
2024-02-26 18:25:52 +08:00
|
|
|
let wrappedFn = fn
|
|
|
|
if (arr !== self) {
|
2024-07-29 09:22:18 +08:00
|
|
|
if (needsWrap) {
|
2024-02-26 18:25:52 +08:00
|
|
|
wrappedFn = function (this: unknown, item, index) {
|
|
|
|
return fn.call(this, toReactive(item), index, self)
|
|
|
|
}
|
|
|
|
} else if (fn.length > 2) {
|
|
|
|
wrappedFn = function (this: unknown, item, index) {
|
|
|
|
return fn.call(this, item, index, self)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-08-10 15:54:38 +08:00
|
|
|
const result = methodFn.call(arr, wrappedFn, thisArg)
|
2024-07-29 09:22:18 +08:00
|
|
|
return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result
|
2024-02-26 18:25:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// instrument reduce and reduceRight to take ARRAY_ITERATE dependency
|
|
|
|
function reduce(
|
|
|
|
self: unknown[],
|
|
|
|
method: keyof Array<any>,
|
|
|
|
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, toReactive(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<any>,
|
|
|
|
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<any>,
|
|
|
|
args: unknown[] = [],
|
|
|
|
) {
|
|
|
|
pauseTracking()
|
|
|
|
startBatch()
|
|
|
|
const res = (toRaw(self) as any)[method].apply(self, args)
|
|
|
|
endBatch()
|
|
|
|
resetTracking()
|
|
|
|
return res
|
|
|
|
}
|