import { bench } from 'vitest'
-import { computed, reactive, readonly, shallowRef, triggerRef } from '../src'
+import { effect, reactive, shallowReadArray } from '../src'
for (let amount = 1e1; amount < 1e4; amount *= 10) {
{
- const rawArray: any[] = []
+ const rawArray: number[] = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
- const r = reactive(rawArray)
- const c = computed(() => {
- return r.reduce((v, a) => a + v, 0)
+ const arr = reactive(rawArray)
+
+ bench(`track for loop, ${amount} elements`, () => {
+ let sum = 0
+ effect(() => {
+ for (let i = 0; i < arr.length; i++) {
+ sum += arr[i]
+ }
+ })
})
+ }
- bench(`reduce *reactive* array, ${amount} elements`, () => {
- for (let i = 0, n = r.length; i < n; i++) {
- r[i]++
- }
- c.value
+ {
+ const rawArray: number[] = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const arr = reactive(rawArray)
+
+ bench(`track manual reactiveReadArray, ${amount} elements`, () => {
+ let sum = 0
+ effect(() => {
+ const raw = shallowReadArray(arr)
+ for (let i = 0; i < raw.length; i++) {
+ sum += raw[i]
+ }
+ })
+ })
+ }
+
+ {
+ const rawArray: number[] = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const arr = reactive(rawArray)
+
+ bench(`track iteration, ${amount} elements`, () => {
+ let sum = 0
+ effect(() => {
+ for (let x of arr) {
+ sum += x
+ }
+ })
+ })
+ }
+
+ {
+ const rawArray: number[] = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const arr = reactive(rawArray)
+
+ bench(`track forEach, ${amount} elements`, () => {
+ let sum = 0
+ effect(() => {
+ arr.forEach(x => (sum += x))
+ })
+ })
+ }
+
+ {
+ const rawArray: number[] = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const arr = reactive(rawArray)
+
+ bench(`track reduce, ${amount} elements`, () => {
+ let sum = 0
+ effect(() => {
+ sum = arr.reduce((v, a) => a + v, 0)
+ })
})
}
rawArray.push(i)
}
const r = reactive(rawArray)
- const c = computed(() => {
- return r.reduce((v, a) => a + v, 0)
- })
+ effect(() => r.reduce((v, a) => a + v, 0))
bench(
- `reduce *reactive* array, ${amount} elements, only change first value`,
+ `trigger index mutation (1st only), tracked with reduce, ${amount} elements`,
() => {
r[0]++
- c.value
},
)
}
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
- const r = reactive({ arr: readonly(rawArray) })
- const c = computed(() => {
- return r.arr.reduce((v, a) => a + v, 0)
- })
+ const r = reactive(rawArray)
+ effect(() => r.reduce((v, a) => a + v, 0))
- bench(`reduce *readonly* array, ${amount} elements`, () => {
- r.arr = r.arr.map(v => v + 1)
- c.value
- })
+ bench(
+ `trigger index mutation (all), tracked with reduce, ${amount} elements`,
+ () => {
+ for (let i = 0, n = r.length; i < n; i++) {
+ r[i]++
+ }
+ },
+ )
}
{
- const rawArray: any[] = []
+ const rawArray: number[] = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
- const r = shallowRef(rawArray)
- const c = computed(() => {
- return r.value.reduce((v, a) => a + v, 0)
+ const arr = reactive(rawArray)
+ let sum = 0
+ effect(() => {
+ for (let x of arr) {
+ sum += x
+ }
})
- bench(`reduce *raw* array, copied, ${amount} elements`, () => {
- r.value = r.value.map(v => v + 1)
- c.value
+ bench(`push() trigger, tracked via iteration, ${amount} elements`, () => {
+ arr.push(1)
})
}
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
- const r = shallowRef(rawArray)
- const c = computed(() => {
- return r.value.reduce((v, a) => a + v, 0)
+ const arr = reactive(rawArray)
+ let sum = 0
+ effect(() => {
+ arr.forEach(x => (sum += x))
})
- bench(`reduce *raw* array, manually triggered, ${amount} elements`, () => {
- for (let i = 0, n = rawArray.length; i < n; i++) {
- rawArray[i]++
- }
- triggerRef(r)
- c.value
+ bench(`push() trigger, tracked via forEach, ${amount} elements`, () => {
+ arr.push(1)
})
}
}
-import { isReactive, reactive, toRaw } from '../src/reactive'
+import { type ComputedRef, computed } from '../src/computed'
+import { isReactive, reactive, shallowReactive, toRaw } from '../src/reactive'
import { isRef, ref } from '../src/ref'
import { effect } from '../src/effect'
expect(observed.lastSearched).toBe(6)
})
})
+
+ describe('Optimized array methods:', () => {
+ test('iterator', () => {
+ const shallow = shallowReactive([1, 2, 3, 4])
+ let result = computed(() => {
+ let sum = 0
+ for (let x of shallow) {
+ sum += x ** 2
+ }
+ return sum
+ })
+ expect(result.value).toBe(30)
+
+ shallow[2] = 0
+ expect(result.value).toBe(21)
+
+ const deep = reactive([{ val: 1 }, { val: 2 }])
+ result = computed(() => {
+ let sum = 0
+ for (let x of deep) {
+ sum += x.val ** 2
+ }
+ return sum
+ })
+ expect(result.value).toBe(5)
+
+ deep[1].val = 3
+ expect(result.value).toBe(10)
+ })
+
+ test('concat', () => {
+ const a1 = shallowReactive([1, { val: 2 }])
+ const a2 = reactive([{ val: 3 }])
+ const a3 = [4, 5]
+
+ let result = computed(() => a1.concat(a2, a3))
+ expect(result.value).toStrictEqual([1, { val: 2 }, { val: 3 }, 4, 5])
+ expect(isReactive(result.value[1])).toBe(false)
+ expect(isReactive(result.value[2])).toBe(true)
+
+ a1.shift()
+ expect(result.value).toStrictEqual([{ val: 2 }, { val: 3 }, 4, 5])
+
+ a2.pop()
+ expect(result.value).toStrictEqual([{ val: 2 }, 4, 5])
+
+ a3.pop()
+ expect(result.value).toStrictEqual([{ val: 2 }, 4, 5])
+ })
+
+ test('entries', () => {
+ const shallow = shallowReactive([0, 1])
+ const result1 = computed(() => Array.from(shallow.entries()))
+ expect(result1.value).toStrictEqual([
+ [0, 0],
+ [1, 1],
+ ])
+
+ shallow[1] = 10
+ expect(result1.value).toStrictEqual([
+ [0, 0],
+ [1, 10],
+ ])
+
+ const deep = reactive([{ val: 0 }, { val: 1 }])
+ const result2 = computed(() => Array.from(deep.entries()))
+ expect(result2.value).toStrictEqual([
+ [0, { val: 0 }],
+ [1, { val: 1 }],
+ ])
+ expect(isReactive(result2.value[0][1])).toBe(true)
+
+ deep.pop()
+ expect(Array.from(result2.value)).toStrictEqual([[0, { val: 0 }]])
+ })
+
+ test('every', () => {
+ const shallow = shallowReactive([1, 2, 5])
+ let result = computed(() => shallow.every(x => x < 5))
+ expect(result.value).toBe(false)
+
+ shallow.pop()
+ expect(result.value).toBe(true)
+
+ const deep = reactive([{ val: 1 }, { val: 5 }])
+ result = computed(() => deep.every(x => x.val < 5))
+ expect(result.value).toBe(false)
+
+ deep[1].val = 2
+ expect(result.value).toBe(true)
+ })
+
+ test('filter', () => {
+ const shallow = shallowReactive([1, 2, 3, 4])
+ const result1 = computed(() => shallow.filter(x => x < 3))
+ expect(result1.value).toStrictEqual([1, 2])
+
+ shallow[2] = 0
+ expect(result1.value).toStrictEqual([1, 2, 0])
+
+ const deep = reactive([{ val: 1 }, { val: 2 }])
+ const result2 = computed(() => deep.filter(x => x.val < 2))
+ expect(result2.value).toStrictEqual([{ val: 1 }])
+ expect(isReactive(result2.value[0])).toBe(true)
+
+ deep[1].val = 0
+ expect(result2.value).toStrictEqual([{ val: 1 }, { val: 0 }])
+ })
+
+ test('find and co.', () => {
+ const shallow = shallowReactive([{ val: 1 }, { val: 2 }])
+ let find = computed(() => shallow.find(x => x.val === 2))
+ // @ts-expect-error tests are not limited to es2016
+ let findLast = computed(() => shallow.findLast(x => x.val === 2))
+ let findIndex = computed(() => shallow.findIndex(x => x.val === 2))
+ let findLastIndex = computed(() =>
+ // @ts-expect-error tests are not limited to es2016
+ shallow.findLastIndex(x => x.val === 2),
+ )
+
+ expect(find.value).toBe(shallow[1])
+ expect(isReactive(find.value)).toBe(false)
+ expect(findLast.value).toBe(shallow[1])
+ expect(isReactive(findLast.value)).toBe(false)
+ expect(findIndex.value).toBe(1)
+ expect(findLastIndex.value).toBe(1)
+
+ shallow[1].val = 0
+
+ expect(find.value).toBe(shallow[1])
+ expect(findLast.value).toBe(shallow[1])
+ expect(findIndex.value).toBe(1)
+ expect(findLastIndex.value).toBe(1)
+
+ shallow.pop()
+
+ expect(find.value).toBe(undefined)
+ expect(findLast.value).toBe(undefined)
+ expect(findIndex.value).toBe(-1)
+ expect(findLastIndex.value).toBe(-1)
+
+ const deep = reactive([{ val: 1 }, { val: 2 }])
+ find = computed(() => deep.find(x => x.val === 2))
+ // @ts-expect-error tests are not limited to es2016
+ findLast = computed(() => deep.findLast(x => x.val === 2))
+ findIndex = computed(() => deep.findIndex(x => x.val === 2))
+ // @ts-expect-error tests are not limited to es2016
+ findLastIndex = computed(() => deep.findLastIndex(x => x.val === 2))
+
+ expect(find.value).toBe(deep[1])
+ expect(isReactive(find.value)).toBe(true)
+ expect(findLast.value).toBe(deep[1])
+ expect(isReactive(findLast.value)).toBe(true)
+ expect(findIndex.value).toBe(1)
+ expect(findLastIndex.value).toBe(1)
+
+ deep[1].val = 0
+
+ expect(find.value).toBe(undefined)
+ expect(findLast.value).toBe(undefined)
+ expect(findIndex.value).toBe(-1)
+ expect(findLastIndex.value).toBe(-1)
+ })
+
+ test('forEach', () => {
+ const shallow = shallowReactive([1, 2, 3, 4])
+ let result = computed(() => {
+ let sum = 0
+ shallow.forEach(x => (sum += x ** 2))
+ return sum
+ })
+ expect(result.value).toBe(30)
+
+ shallow[2] = 0
+ expect(result.value).toBe(21)
+
+ const deep = reactive([{ val: 1 }, { val: 2 }])
+ result = computed(() => {
+ let sum = 0
+ deep.forEach(x => (sum += x.val ** 2))
+ return sum
+ })
+ expect(result.value).toBe(5)
+
+ deep[1].val = 3
+ expect(result.value).toBe(10)
+ })
+
+ test('join', () => {
+ function toString(this: { val: number }) {
+ return this.val
+ }
+ const shallow = shallowReactive([
+ { val: 1, toString },
+ { val: 2, toString },
+ ])
+ let result = computed(() => shallow.join('+'))
+ expect(result.value).toBe('1+2')
+
+ shallow[1].val = 23
+ expect(result.value).toBe('1+2')
+
+ shallow.pop()
+ expect(result.value).toBe('1')
+
+ const deep = reactive([
+ { val: 1, toString },
+ { val: 2, toString },
+ ])
+ result = computed(() => deep.join())
+ expect(result.value).toBe('1,2')
+
+ deep[1].val = 23
+ expect(result.value).toBe('1,23')
+ })
+
+ test('map', () => {
+ const shallow = shallowReactive([1, 2, 3, 4])
+ let result = computed(() => shallow.map(x => x ** 2))
+ expect(result.value).toStrictEqual([1, 4, 9, 16])
+
+ shallow[2] = 0
+ expect(result.value).toStrictEqual([1, 4, 0, 16])
+
+ const deep = reactive([{ val: 1 }, { val: 2 }])
+ result = computed(() => deep.map(x => x.val ** 2))
+ expect(result.value).toStrictEqual([1, 4])
+
+ deep[1].val = 3
+ expect(result.value).toStrictEqual([1, 9])
+ })
+
+ test('reduce left and right', () => {
+ function toString(this: any) {
+ return this.val + '-'
+ }
+ const shallow = shallowReactive([
+ { val: 1, toString },
+ { val: 2, toString },
+ ] as any[])
+
+ expect(shallow.reduce((acc, x) => acc + '' + x.val, undefined)).toBe(
+ 'undefined12',
+ )
+
+ let left = computed(() => shallow.reduce((acc, x) => acc + '' + x.val))
+ let right = computed(() =>
+ shallow.reduceRight((acc, x) => acc + '' + x.val),
+ )
+ expect(left.value).toBe('1-2')
+ expect(right.value).toBe('2-1')
+
+ shallow[1].val = 23
+ expect(left.value).toBe('1-2')
+ expect(right.value).toBe('2-1')
+
+ shallow.pop()
+ expect(left.value).toBe(shallow[0])
+ expect(right.value).toBe(shallow[0])
+
+ const deep = reactive([{ val: 1 }, { val: 2 }])
+ left = computed(() => deep.reduce((acc, x) => acc + x.val, '0'))
+ right = computed(() => deep.reduceRight((acc, x) => acc + x.val, '3'))
+ expect(left.value).toBe('012')
+ expect(right.value).toBe('321')
+
+ deep[1].val = 23
+ expect(left.value).toBe('0123')
+ expect(right.value).toBe('3231')
+ })
+
+ test('some', () => {
+ const shallow = shallowReactive([1, 2, 5])
+ let result = computed(() => shallow.some(x => x > 4))
+ expect(result.value).toBe(true)
+
+ shallow.pop()
+ expect(result.value).toBe(false)
+
+ const deep = reactive([{ val: 1 }, { val: 5 }])
+ result = computed(() => deep.some(x => x.val > 4))
+ expect(result.value).toBe(true)
+
+ deep[1].val = 2
+ expect(result.value).toBe(false)
+ })
+
+ // Node 20+
+ // @ts-expect-error tests are not limited to es2016
+ test.skipIf(!Array.prototype.toReversed)('toReversed', () => {
+ const array = reactive([1, { val: 2 }])
+ const result = computed(() => (array as any).toReversed())
+ expect(result.value).toStrictEqual([{ val: 2 }, 1])
+ expect(isReactive(result.value[0])).toBe(true)
+
+ array.splice(1, 1, 2)
+ expect(result.value).toStrictEqual([2, 1])
+ })
+
+ // Node 20+
+ // @ts-expect-error tests are not limited to es2016
+ test.skipIf(!Array.prototype.toSorted)('toSorted', () => {
+ // No comparer
+ // @ts-expect-error
+ expect(shallowReactive([2, 1, 3]).toSorted()).toStrictEqual([1, 2, 3])
+
+ const shallow = shallowReactive([{ val: 2 }, { val: 1 }, { val: 3 }])
+ let result: ComputedRef<{ val: number }[]>
+ // @ts-expect-error
+ result = computed(() => shallow.toSorted((a, b) => a.val - b.val))
+ expect(result.value.map(x => x.val)).toStrictEqual([1, 2, 3])
+ expect(isReactive(result.value[0])).toBe(false)
+
+ shallow[0].val = 4
+ expect(result.value.map(x => x.val)).toStrictEqual([1, 4, 3])
+
+ shallow.pop()
+ expect(result.value.map(x => x.val)).toStrictEqual([1, 4])
+
+ const deep = reactive([{ val: 2 }, { val: 1 }, { val: 3 }])
+ // @ts-expect-error
+ result = computed(() => deep.toSorted((a, b) => a.val - b.val))
+ expect(result.value.map(x => x.val)).toStrictEqual([1, 2, 3])
+ expect(isReactive(result.value[0])).toBe(true)
+
+ deep[0].val = 4
+ expect(result.value.map(x => x.val)).toStrictEqual([1, 3, 4])
+ })
+
+ // Node 20+
+ // @ts-expect-error tests are not limited to es2016
+ test.skipIf(!Array.prototype.toSpliced)('toSpliced', () => {
+ const array = reactive([1, 2, 3])
+ // @ts-expect-error
+ const result = computed(() => array.toSpliced(1, 1, -2))
+ expect(result.value).toStrictEqual([1, -2, 3])
+
+ array[0] = 0
+ expect(result.value).toStrictEqual([0, -2, 3])
+ })
+
+ test('values', () => {
+ const shallow = shallowReactive([{ val: 1 }, { val: 2 }])
+ const result = computed(() => Array.from(shallow.values()))
+ expect(result.value).toStrictEqual([{ val: 1 }, { val: 2 }])
+ expect(isReactive(result.value[0])).toBe(false)
+
+ shallow.pop()
+ expect(result.value).toStrictEqual([{ val: 1 }])
+
+ const deep = reactive([{ val: 1 }, { val: 2 }])
+ const firstItem = Array.from(deep.values())[0]
+ expect(isReactive(firstItem)).toBe(true)
+ })
+ })
})
--- /dev/null
+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'
+
+/**
+ * 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)
+ },
+
+ concat(...args: unknown[][]) {
+ return reactiveReadArray(this).concat(
+ ...args.map(x => reactiveReadArray(x)),
+ )
+ },
+
+ 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,
+ ) {
+ return apply(this, 'every', fn, thisArg)
+ },
+
+ filter(
+ fn: (item: unknown, index: number, array: unknown[]) => unknown,
+ thisArg?: unknown,
+ ) {
+ const result = apply(this, 'filter', fn, thisArg)
+ return isProxy(this) && !isShallow(this) ? result.map(toReactive) : result
+ },
+
+ find(
+ fn: (item: unknown, index: number, array: unknown[]) => boolean,
+ thisArg?: unknown,
+ ) {
+ const result = apply(this, 'find', fn, thisArg)
+ return isProxy(this) && !isShallow(this) ? toReactive(result) : result
+ },
+
+ findIndex(
+ fn: (item: unknown, index: number, array: unknown[]) => boolean,
+ thisArg?: unknown,
+ ) {
+ return apply(this, 'findIndex', fn, thisArg)
+ },
+
+ findLast(
+ fn: (item: unknown, index: number, array: unknown[]) => boolean,
+ thisArg?: unknown,
+ ) {
+ const result = apply(this, 'findLast', fn, thisArg)
+ return isProxy(this) && !isShallow(this) ? toReactive(result) : result
+ },
+
+ findLastIndex(
+ fn: (item: unknown, index: number, array: unknown[]) => boolean,
+ thisArg?: unknown,
+ ) {
+ return apply(this, 'findLastIndex', fn, thisArg)
+ },
+
+ // 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)
+ },
+
+ 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,
+ ) {
+ return apply(this, 'map', fn, thisArg)
+ },
+
+ 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)
+ },
+
+ 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[],
+ method: keyof Array<any>,
+ 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)
+ const iter = (arr[method] as any)()
+ if (arr !== self && !isShallow(self)) {
+ ;(iter as any)._next = iter.next
+ iter.next = () => {
+ const result = (iter as any)._next()
+ 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'
+
+// 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,
+) {
+ const arr = shallowReadArray(self)
+ let wrappedFn = fn
+ if (arr !== self) {
+ if (!isShallow(self)) {
+ 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)
+ }
+ }
+ }
+ // @ts-expect-error our code is limited to es2016 but user code is not
+ return arr[method](wrappedFn, thisArg)
+}
+
+// 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
+}
shallowReadonlyMap,
toRaw,
} from './reactive'
+import { arrayInstrumentations } from './arrayInstrumentations'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { ITERATE_KEY, track, trigger } from './dep'
import {
} from '@vue/shared'
import { isRef } from './ref'
import { warn } from './warning'
-import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
.filter(isSymbol),
)
-const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
-
-function createArrayInstrumentations() {
- const instrumentations: Record<string, Function> = {}
- // instrument identity-sensitive Array methods to account for possible reactive
- // values
- ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
- instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
- 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
- }
- }
- })
- // instrument length-altering mutation methods to avoid length being tracked
- // which leads to infinite loops in some cases (#2137)
- ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
- instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
- startBatch()
- pauseTracking()
- const res = (toRaw(this) as any)[key].apply(this, args)
- resetTracking()
- endBatch()
- return res
- }
- })
- return instrumentations
-}
-
function hasOwnProperty(this: object, key: string) {
const obj = toRaw(this)
track(obj, TrackOpTypes.HAS, key)
const targetIsArray = isArray(target)
if (!isReadonly) {
- if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
- return Reflect.get(arrayInstrumentations, key, receiver)
+ let fn: Function | undefined
+ if (targetIsArray && (fn = arrayInstrumentations[key])) {
+ return fn
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<object, KeyToDepMap>()
-export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
-export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map iterate' : '')
+export const ITERATE_KEY = Symbol(__DEV__ ? 'Object iterate' : '')
+export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map keys iterate' : '')
+export const ARRAY_ITERATE_KEY = Symbol(__DEV__ ? 'Array iterate' : '')
/**
* Tracks access to a reactive property.
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
- } else if (key === 'length' && isArray(target)) {
- const newLength = Number(newValue)
- depsMap.forEach((dep, key) => {
- if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
- deps.push(dep)
- }
- })
} else {
- const push = (dep: Dep | undefined) => dep && deps.push(dep)
+ const targetIsArray = isArray(target)
+ const isArrayIndex = targetIsArray && isIntegerKey(key)
- // schedule runs for SET | ADD | DELETE
- if (key !== void 0) {
- push(depsMap.get(key))
- }
+ if (targetIsArray && key === 'length') {
+ const newLength = Number(newValue)
+ depsMap.forEach((dep, key) => {
+ if (
+ key === 'length' ||
+ key === ARRAY_ITERATE_KEY ||
+ (!isSymbol(key) && key >= newLength)
+ ) {
+ deps.push(dep)
+ }
+ })
+ } else {
+ const push = (dep: Dep | undefined) => dep && deps.push(dep)
- // also run for iteration key on ADD | DELETE | Map.SET
- switch (type) {
- case TriggerOpTypes.ADD:
- if (!isArray(target)) {
- push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- push(depsMap.get(MAP_KEY_ITERATE_KEY))
+ // schedule runs for SET | ADD | DELETE
+ if (key !== void 0) {
+ push(depsMap.get(key))
+ }
+
+ // schedule ARRAY_ITERATE for any numeric key change (length is handled above)
+ if (isArrayIndex) {
+ push(depsMap.get(ARRAY_ITERATE_KEY))
+ }
+
+ // also run for iteration key on ADD | DELETE | Map.SET
+ switch (type) {
+ case TriggerOpTypes.ADD:
+ if (!targetIsArray) {
+ push(depsMap.get(ITERATE_KEY))
+ if (isMap(target)) {
+ push(depsMap.get(MAP_KEY_ITERATE_KEY))
+ }
+ } else if (isArrayIndex) {
+ // new index added to array -> length changes
+ push(depsMap.get('length'))
}
- } else if (isIntegerKey(key)) {
- // new index added to array -> length changes
- push(depsMap.get('length'))
- }
- break
- case TriggerOpTypes.DELETE:
- if (!isArray(target)) {
- push(depsMap.get(ITERATE_KEY))
+ break
+ case TriggerOpTypes.DELETE:
+ if (!targetIsArray) {
+ push(depsMap.get(ITERATE_KEY))
+ if (isMap(target)) {
+ push(depsMap.get(MAP_KEY_ITERATE_KEY))
+ }
+ }
+ break
+ case TriggerOpTypes.SET:
if (isMap(target)) {
- push(depsMap.get(MAP_KEY_ITERATE_KEY))
+ push(depsMap.get(ITERATE_KEY))
}
- }
- break
- case TriggerOpTypes.SET:
- if (isMap(target)) {
- push(depsMap.get(ITERATE_KEY))
- }
- break
+ break
+ }
}
}
shallowReadonly,
markRaw,
toRaw,
+ toReactive,
+ toReadonly,
type Raw,
type DeepReadonly,
type ShallowReactive,
type DebuggerEvent,
type DebuggerEventExtraInfo,
} from './effect'
-export { trigger, track, ITERATE_KEY } from './dep'
+export {
+ trigger,
+ track,
+ ITERATE_KEY,
+ ARRAY_ITERATE_KEY,
+ MAP_KEY_ITERATE_KEY,
+} from './dep'
export {
effectScope,
EffectScope,
getCurrentScope,
onScopeDispose,
} from './effectScope'
+export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
import type { VNode, VNodeChild } from '../vnode'
+import { isReactive, shallowReadArray, toReactive } from '@vue/reactivity'
import { isArray, isObject, isString } from '@vue/shared'
import { warn } from '../warning'
): VNodeChild[] {
let ret: VNodeChild[]
const cached = (cache && cache[index!]) as VNode[] | undefined
+ const sourceIsArray = isArray(source)
+ const sourceIsReactiveArray = sourceIsArray && isReactive(source)
- if (isArray(source) || isString(source)) {
+ if (sourceIsArray || isString(source)) {
+ if (sourceIsReactiveArray) {
+ source = shallowReadArray(source)
+ }
ret = new Array(source.length)
for (let i = 0, l = source.length; i < l; i++) {
- ret[i] = renderItem(source[i], i, undefined, cached && cached[i])
+ ret[i] = renderItem(
+ sourceIsReactiveArray ? toReactive(source[i]) : source[i],
+ i,
+ undefined,
+ cached && cached[i],
+ )
}
} else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) {