perf(reactivity): optimize array tracking (#9511)

close #4318
This commit is contained in:
jods 2024-02-26 11:25:52 +01:00 committed by GitHub
parent 72bde94e66
commit 70196a40cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 851 additions and 122 deletions

View File

@ -1,22 +1,86 @@
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(`reduce *reactive* array, ${amount} elements`, () => {
for (let i = 0, n = r.length; i < n; i++) {
r[i]++
}
c.value
bench(`track for loop, ${amount} elements`, () => {
let sum = 0
effect(() => {
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
})
})
}
{
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)
})
})
}
@ -26,15 +90,12 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
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
},
)
}
@ -44,30 +105,34 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
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)
})
}
@ -76,17 +141,14 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
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)
})
}
}

View File

@ -1,4 +1,5 @@
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'
@ -252,4 +253,359 @@ describe('reactivity/reactive/Array', () => {
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)
})
})
})

View File

@ -0,0 +1,312 @@
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
}

View File

@ -10,6 +10,7 @@ import {
shallowReadonlyMap,
toRaw,
} from './reactive'
import { arrayInstrumentations } from './arrayInstrumentations'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { ITERATE_KEY, track, trigger } from './dep'
import {
@ -23,7 +24,6 @@ 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`)
@ -38,43 +38,6 @@ const builtInSymbols = new Set(
.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)
@ -120,8 +83,9 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
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

View File

@ -162,8 +162,9 @@ function addSub(link: Link) {
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.
@ -225,47 +226,61 @@ export function trigger(
// 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))
if (isMap(target)) {
push(depsMap.get(MAP_KEY_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(ITERATE_KEY))
}
break
break
case TriggerOpTypes.SET:
if (isMap(target)) {
push(depsMap.get(ITERATE_KEY))
}
break
}
}
}

View File

@ -31,6 +31,8 @@ export {
shallowReadonly,
markRaw,
toRaw,
toReactive,
toReadonly,
type Raw,
type DeepReadonly,
type ShallowReactive,
@ -60,11 +62,18 @@ export {
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'

View File

@ -1,4 +1,5 @@
import type { VNode, VNodeChild } from '../vnode'
import { isReactive, shallowReadArray, toReactive } from '@vue/reactivity'
import { isArray, isObject, isString } from '@vue/shared'
import { warn } from '../warning'
@ -58,11 +59,21 @@ export function renderList(
): 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)) {