From 9f18a05a32ed6334da9492671278e3563942ad84 Mon Sep 17 00:00:00 2001 From: jods4 Date: Mon, 30 Oct 2023 23:30:14 +0100 Subject: [PATCH 1/6] perf(reactivity): optimize array tracking --- .../__tests__/reactiveArray.spec.ts | 346 +++++++++++++++++- .../reactivity/src/arrayInstrumentations.ts | 297 +++++++++++++++ packages/reactivity/src/baseHandlers.ts | 49 +-- packages/reactivity/src/index.ts | 8 +- packages/reactivity/src/reactiveEffect.ts | 15 +- .../runtime-core/src/helpers/renderList.ts | 7 +- 6 files changed, 673 insertions(+), 49 deletions(-) create mode 100644 packages/reactivity/src/arrayInstrumentations.ts diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts index f4eb7b58384..7ea96daacf4 100644 --- a/packages/reactivity/__tests__/reactiveArray.spec.ts +++ b/packages/reactivity/__tests__/reactiveArray.spec.ts @@ -1,4 +1,5 @@ -import { reactive, isReactive, toRaw } from '../src/reactive' +import { computed } from '../src/computed' +import { reactive, shallowReactive, isReactive, toRaw } from '../src/reactive' import { ref, isRef } from '../src/ref' import { effect } from '../src/effect' @@ -243,4 +244,347 @@ 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)) + let findLast = computed(() => shallow.findLast(x => x.val === 2)) + let findIndex = computed(() => shallow.findIndex(x => x.val === 2)) + let findLastIndex = computed(() => + 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)) + findLast = computed(() => deep.findLast(x => x.val === 2)) + findIndex = computed(() => deep.findIndex(x => x.val === 2)) + 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+ + test.skipIf(!Array.prototype.toReversed)('toReversed', () => { + const array = reactive([1, { val: 2 }]) + const result = computed(() => array.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+ + test.skipIf(!Array.prototype.toSorted)('toSorted', () => { + // No comparer + expect(shallowReactive([2, 1, 3]).toSorted()).toStrictEqual([1, 2, 3]) + + const shallow = shallowReactive([{ val: 2 }, { val: 1 }, { val: 3 }]) + let 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 }]) + 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+ + test.skipIf(!Array.prototype.toSpliced)('toSpliced', () => { + const array = reactive([1, 2, 3]) + 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) + }) + }) }) diff --git a/packages/reactivity/src/arrayInstrumentations.ts b/packages/reactivity/src/arrayInstrumentations.ts new file mode 100644 index 00000000000..82ca640565b --- /dev/null +++ b/packages/reactivity/src/arrayInstrumentations.ts @@ -0,0 +1,297 @@ +import { TrackOpTypes } from './constants' +import { + pauseTracking, + resetTracking, + pauseScheduling, + resetScheduling +} from './effect' +import { isProxy, isShallow, toRaw, toReactive } from './reactive' +import { track, ARRAY_ITERATE_KEY } from './reactiveEffect' + +export function readArray(array: T[], deep = false) { + const arr = toRaw(array) + if (arr === array) { + return arr + } + track(arr, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY) + return !deep || isShallow(array) ? arr : arr.map(toReactive) +} + +export const arrayInstrumentations: Record = { + __proto__: null, + + [Symbol.iterator]() { + return iterator(this, Symbol.iterator, toReactive) + }, + + concat(...args: unknown[][]) { + const arr = readArray(this, true) + return arr.concat(...args.map(x => readArray(x, true))) + }, + + 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 callback(this, 'every', fn, thisArg) + }, + + filter( + fn: (item: unknown, index: number, array: unknown[]) => unknown, + thisArg?: unknown + ) { + const result = callback(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 = callback(this, 'find', fn, thisArg) + return isProxy(this) && !isShallow(this) ? toReactive(result) : result + }, + + findIndex( + fn: (item: unknown, index: number, array: unknown[]) => boolean, + thisArg?: unknown + ) { + return callback(this, 'findIndex', fn, thisArg) + }, + + findLast( + fn: (item: unknown, index: number, array: unknown[]) => boolean, + thisArg?: unknown + ) { + const result = callback(this, 'findLast', fn, thisArg) + return isProxy(this) && !isShallow(this) ? toReactive(result) : result + }, + + findLastIndex( + fn: (item: unknown, index: number, array: unknown[]) => boolean, + thisArg?: unknown + ) { + return callback(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 callback(this, 'forEach', fn, thisArg) + }, + + includes(...args: unknown[]) { + return searchProxy(this, 'includes', args) + }, + + indexOf(...args: unknown[]) { + return searchProxy(this, 'indexOf', args) + }, + + join(separator?: string) { + return readArray(this, true).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 callback(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 callback(this, 'some', fn, thisArg) + }, + + splice(...args: unknown[]) { + return noTracking(this, 'splice', args) + }, + + toReversed() { + return readArray(this, true).toReversed() + }, + + toSorted(comparer?: Function) { + return readArray(this, true).toSorted(comparer as any) + }, + + toSpliced(...args: unknown[]) { + return (readArray(this, true).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, + 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 = readArray(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 +} + +// instrument functions that read (potentially) all items +// to take ARRAY_ITERATE dependency +function callback( + self: unknown[], + method: keyof Array, + fn: (item: unknown, index: number, array: unknown[]) => unknown, + thisArg?: unknown +) { + const arr = readArray(self) + let fn2 = fn + if (arr !== self) { + if (!isShallow(self)) { + fn2 = function (this: unknown, item, index) { + return fn.call(this, toReactive(item), index, self) + } + } else if (fn.length > 2) { + fn2 = function (this: unknown, item, index) { + return fn.call(this, item, index, self) + } + } + } + return (arr[method] as any)(fn2, thisArg) +} + +// 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 = readArray(self) + let fn2 = fn + if (arr !== self) { + if (!isShallow(self)) { + fn2 = function (this: unknown, acc, item, index) { + return fn.call(this, acc, toReactive(item), index, self) + } + } else if (fn.length > 3) { + fn2 = function (this: unknown, acc, item, index) { + return fn.call(this, acc, item, index, self) + } + } + } + return (arr[method] as any)(fn2, ...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[] = [] +) { + pauseTracking() + pauseScheduling() + const res = (toRaw(self) as any)[method].apply(self, args) + resetScheduling() + resetTracking() + return res +} diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 36e4d311b4b..b62a77c9906 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -10,13 +10,8 @@ import { isReadonly, isShallow } from './reactive' +import { arrayInstrumentations } from './arrayInstrumentations' import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' -import { - pauseTracking, - resetTracking, - pauseScheduling, - resetScheduling -} from './effect' import { track, trigger, ITERATE_KEY } from './reactiveEffect' import { isObject, @@ -43,43 +38,6 @@ const builtInSymbols = new Set( .filter(isSymbol) ) -const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations() - -function createArrayInstrumentations() { - const instrumentations: Record = {} - // 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[]) { - pauseTracking() - pauseScheduling() - const res = (toRaw(this) as any)[key].apply(this, args) - resetScheduling() - resetTracking() - return res - } - }) - return instrumentations -} - function hasOwnProperty(this: object, key: string) { const obj = toRaw(this) track(obj, TrackOpTypes.HAS, key) @@ -119,8 +77,9 @@ class BaseReactiveHandler implements ProxyHandler { 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 diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 9497527e81e..0e0601dcd2a 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -61,7 +61,12 @@ export { type DebuggerEvent, type DebuggerEventExtraInfo } from './effect' -export { trigger, track, ITERATE_KEY } from './reactiveEffect' +export { + trigger, + track, + ITERATE_KEY, + ARRAY_ITERATE_KEY +} from './reactiveEffect' export { effectScope, EffectScope, @@ -73,3 +78,4 @@ export { TriggerOpTypes /* @remove */, ReactiveFlags /* @remove */ } from './constants' +export { readArray } from './arrayInstrumentations' diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts index d3474db3da1..b96ad1905a6 100644 --- a/packages/reactivity/src/reactiveEffect.ts +++ b/packages/reactivity/src/reactiveEffect.ts @@ -18,6 +18,7 @@ type KeyToDepMap = Map const targetMap = new WeakMap() export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') +export const ARRAY_ITERATE_KEY = Symbol(__DEV__ ? 'Array iterate' : '') export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') /** @@ -84,7 +85,11 @@ export function trigger( } else if (key === 'length' && isArray(target)) { const newLength = Number(newValue) depsMap.forEach((dep, key) => { - if (key === 'length' || (!isSymbol(key) && key >= newLength)) { + if ( + key === 'length' || + key === ARRAY_ITERATE_KEY || + (!isSymbol(key) && key >= newLength) + ) { deps.push(dep) } }) @@ -94,6 +99,14 @@ export function trigger( deps.push(depsMap.get(key)) } + // schedule ARRAY_ITERATE for any numeric key change (length is handled above) + if (isArray(target)) { + const iterateDeps = depsMap.get(ARRAY_ITERATE_KEY) + if (iterateDeps && isIntegerKey(key)) { + deps.push(iterateDeps) + } + } + // also run for iteration key on ADD | DELETE | Map.SET switch (type) { case TriggerOpTypes.ADD: diff --git a/packages/runtime-core/src/helpers/renderList.ts b/packages/runtime-core/src/helpers/renderList.ts index 1655d555fb3..76da68abfd1 100644 --- a/packages/runtime-core/src/helpers/renderList.ts +++ b/packages/runtime-core/src/helpers/renderList.ts @@ -1,4 +1,5 @@ import { VNode, VNodeChild } from '../vnode' +import { readArray } from '@vue/reactivity' import { isArray, isString, isObject } from '@vue/shared' import { warn } from '../warning' @@ -58,8 +59,12 @@ export function renderList( ): VNodeChild[] { let ret: VNodeChild[] const cached = (cache && cache[index!]) as VNode[] | undefined + const array = isArray(source) - if (isArray(source) || isString(source)) { + if (array || isString(source)) { + if (array) { + source = readArray(source, true) + } 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]) From 100e3760cb34b9aab987843bec227306ba89564a Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 26 Feb 2024 15:57:26 +0800 Subject: [PATCH 2/6] wip: tests passing --- .../reactivity/src/arrayInstrumentations.ts | 31 +++---- packages/reactivity/src/dep.ts | 92 ++++++++++--------- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/packages/reactivity/src/arrayInstrumentations.ts b/packages/reactivity/src/arrayInstrumentations.ts index 708bef6ff86..8d4214ae88c 100644 --- a/packages/reactivity/src/arrayInstrumentations.ts +++ b/packages/reactivity/src/arrayInstrumentations.ts @@ -3,22 +3,16 @@ import { endBatch, pauseTracking, resetTracking, startBatch } from './effect' import { isProxy, isShallow, toRaw, toReactive } from './reactive' import { ARRAY_ITERATE_KEY, track } from './dep' -export function reactiveReadArray(array: T[], forceClone = false) { - const arr = toRaw(array) - if (arr === array) { - return arr - } - track(arr, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY) - return isShallow(array) - ? forceClone - ? arr.slice() - : arr - : arr.map(toReactive) +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) } function shallowReadArray(arr: T[]): T[] { - track(arr, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY) - return toRaw(arr) + track((arr = toRaw(arr)), TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY) + return arr } export const arrayInstrumentations: Record = { @@ -170,17 +164,18 @@ export const arrayInstrumentations: Record = { }, toReversed() { - return reactiveReadArray(this, true /* forceClone */).reverse() + // @ts-expect-error user code may run in es2016+ + return reactiveReadArray(this).toReversed() }, toSorted(comparer?: (a: unknown, b: unknown) => number) { - return reactiveReadArray(this, true /* forceClone */).sort(comparer) + // @ts-expect-error user code may run in es2016+ + return reactiveReadArray(this).toSorted(comparer) }, toSpliced(...args: unknown[]) { - return (reactiveReadArray(this, true /* forceClone */).splice as any)( - ...args, - ) + // @ts-expect-error user code may run in es2016+ + return (reactiveReadArray(this).toSpliced as any)(...args) }, unshift(...args: unknown[]) { diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index 9a3c83751e8..0dccf40aaba 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -226,59 +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' || - key === ARRAY_ITERATE_KEY || - (!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) - // schedule ARRAY_ITERATE for any numeric key change (length is handled above) - if (isArray(target)) { - const iterateDep = depsMap.get(ARRAY_ITERATE_KEY) - if (iterateDep && isIntegerKey(key)) { - deps.push(iterateDep) + // schedule runs for SET | ADD | DELETE + if (key !== void 0) { + push(depsMap.get(key)) } - } - // 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 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 + } } } From f257b38aca647ac69c158f97f4952b2d5fc812bb Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 26 Feb 2024 16:22:46 +0800 Subject: [PATCH 3/6] refactor: small renames --- .../reactivity/src/arrayInstrumentations.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/reactivity/src/arrayInstrumentations.ts b/packages/reactivity/src/arrayInstrumentations.ts index 8d4214ae88c..d1df1052be5 100644 --- a/packages/reactivity/src/arrayInstrumentations.ts +++ b/packages/reactivity/src/arrayInstrumentations.ts @@ -39,14 +39,14 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { - return callback(this, 'every', fn, thisArg) + return apply(this, 'every', fn, thisArg) }, filter( fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { - const result = callback(this, 'filter', fn, thisArg) + const result = apply(this, 'filter', fn, thisArg) return isProxy(this) && !isShallow(this) ? result.map(toReactive) : result }, @@ -54,7 +54,7 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { - const result = callback(this, 'find', fn, thisArg) + const result = apply(this, 'find', fn, thisArg) return isProxy(this) && !isShallow(this) ? toReactive(result) : result }, @@ -62,14 +62,14 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { - return callback(this, 'findIndex', fn, thisArg) + return apply(this, 'findIndex', fn, thisArg) }, findLast( fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { - const result = callback(this, 'findLast', fn, thisArg) + const result = apply(this, 'findLast', fn, thisArg) return isProxy(this) && !isShallow(this) ? toReactive(result) : result }, @@ -77,7 +77,7 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { - return callback(this, 'findLastIndex', fn, thisArg) + return apply(this, 'findLastIndex', fn, thisArg) }, // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement @@ -86,7 +86,7 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { - return callback(this, 'forEach', fn, thisArg) + return apply(this, 'forEach', fn, thisArg) }, includes(...args: unknown[]) { @@ -111,7 +111,7 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { - return callback(this, 'map', fn, thisArg) + return apply(this, 'map', fn, thisArg) }, pop() { @@ -156,7 +156,7 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { - return callback(this, 'some', fn, thisArg) + return apply(this, 'some', fn, thisArg) }, splice(...args: unknown[]) { @@ -222,27 +222,27 @@ type ArrayMethods = keyof Array | 'findLast' | 'findLastIndex' // instrument functions that read (potentially) all items // to take ARRAY_ITERATE dependency -function callback( +function apply( self: unknown[], method: ArrayMethods, fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { const arr = shallowReadArray(self) - let fn2 = fn + let wrappedFn = fn if (arr !== self) { if (!isShallow(self)) { - fn2 = function (this: unknown, item, index) { + wrappedFn = function (this: unknown, item, index) { return fn.call(this, toReactive(item), index, self) } } else if (fn.length > 2) { - fn2 = function (this: unknown, item, index) { + wrappedFn = function (this: unknown, item, index) { return fn.call(this, item, index, self) } } } - // @ts-expect-error - return arr[method](fn2, thisArg) + // @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 @@ -253,19 +253,19 @@ function reduce( args: unknown[], ) { const arr = shallowReadArray(self) - let fn2 = fn + let wrappedFn = fn if (arr !== self) { if (!isShallow(self)) { - fn2 = function (this: unknown, acc, item, index) { + wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, toReactive(item), index, self) } } else if (fn.length > 3) { - fn2 = function (this: unknown, acc, item, index) { + wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, item, index, self) } } } - return (arr[method] as any)(fn2, ...args) + return (arr[method] as any)(wrappedFn, ...args) } // instrument identity-sensitive methods to account for reactive proxies From 924431d40abd8356693eb5037248fe6e2ed97244 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 26 Feb 2024 17:02:01 +0800 Subject: [PATCH 4/6] chore: improve reactiveArray benchmark --- .../__benchmarks__/reactiveArray.bench.ts | 203 ++++++++++++++---- 1 file changed, 162 insertions(+), 41 deletions(-) diff --git a/packages/reactivity/__benchmarks__/reactiveArray.bench.ts b/packages/reactivity/__benchmarks__/reactiveArray.bench.ts index 6726cccfd89..381fb1658c1 100644 --- a/packages/reactivity/__benchmarks__/reactiveArray.bench.ts +++ b/packages/reactivity/__benchmarks__/reactiveArray.bench.ts @@ -1,22 +1,139 @@ import { bench } from 'vitest' -import { computed, reactive, readonly, shallowRef, triggerRef } from '../src' +import { + computed, + effect, + reactive, + readonly, + shallowRef, + triggerRef, +} 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 on reactive array, ${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 iteration on reactive array, ${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 on reactive array, ${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 on reactive array, ${amount} elements`, () => { + let sum = 0 + effect(() => { + sum = arr.reduce((v, a) => a + v, 0) + }) + }) + } + + { + const rawArray: number[] = [] + for (let i = 0, n = amount; i < n; i++) { + rawArray.push(i) + } + const arr = readonly(rawArray) + + bench(`track for loop on readonly array, ${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 = readonly(rawArray) + + bench(`track iteration on readonly array, ${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 = readonly(rawArray) + + bench(`track forEach on readonly array, ${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 = readonly(rawArray) + + bench(`track reduce on readonly array, ${amount} elements`, () => { + let sum = 0 + effect(() => { + sum = arr.reduce((v, a) => a + v, 0) + }) }) } @@ -26,15 +143,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) on *reactive* array (tracked with reduce), ${amount} elements`, () => { r[0]++ - c.value }, ) } @@ -44,31 +158,38 @@ 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) on *reactive* array (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 on reactive array tracked via iteration, ${amount} elements`, + () => { + arr.push(1) + }, + ) } { @@ -76,17 +197,17 @@ 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 on reactive array tracked via forEach, ${amount} elements`, + () => { + arr.push(1) + }, + ) } } From b868e15d59819b754a191db74eeb919f5cfd317d Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 26 Feb 2024 17:17:11 +0800 Subject: [PATCH 5/6] wip: benchmark manual read --- .../__benchmarks__/reactiveArray.bench.ts | 97 ++++--------------- .../reactivity/src/arrayInstrumentations.ts | 10 +- packages/reactivity/src/index.ts | 2 +- 3 files changed, 29 insertions(+), 80 deletions(-) diff --git a/packages/reactivity/__benchmarks__/reactiveArray.bench.ts b/packages/reactivity/__benchmarks__/reactiveArray.bench.ts index 381fb1658c1..93fa61501de 100644 --- a/packages/reactivity/__benchmarks__/reactiveArray.bench.ts +++ b/packages/reactivity/__benchmarks__/reactiveArray.bench.ts @@ -1,12 +1,5 @@ import { bench } from 'vitest' -import { - computed, - effect, - reactive, - readonly, - shallowRef, - triggerRef, -} from '../src' +import { effect, reactive, reactiveReadArray, shallowReadArray } from '../src' for (let amount = 1e1; amount < 1e4; amount *= 10) { { @@ -16,7 +9,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { } const arr = reactive(rawArray) - bench(`track for loop on reactive array, ${amount} elements`, () => { + bench(`track for loop, ${amount} elements`, () => { let sum = 0 effect(() => { for (let i = 0; i < arr.length; i++) { @@ -33,11 +26,12 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { } const arr = reactive(rawArray) - bench(`track iteration on reactive array, ${amount} elements`, () => { + bench(`track manual reactiveReadArray, ${amount} elements`, () => { let sum = 0 effect(() => { - for (let x of arr) { - sum += x + const raw = shallowReadArray(arr) + for (let i = 0; i < raw.length; i++) { + sum += raw[i] } }) }) @@ -50,54 +44,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { } const arr = reactive(rawArray) - bench(`track forEach on reactive array, ${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 on reactive array, ${amount} elements`, () => { - let sum = 0 - effect(() => { - sum = arr.reduce((v, a) => a + v, 0) - }) - }) - } - - { - const rawArray: number[] = [] - for (let i = 0, n = amount; i < n; i++) { - rawArray.push(i) - } - const arr = readonly(rawArray) - - bench(`track for loop on readonly array, ${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 = readonly(rawArray) - - bench(`track iteration on readonly array, ${amount} elements`, () => { + bench(`track iteration, ${amount} elements`, () => { let sum = 0 effect(() => { for (let x of arr) { @@ -112,9 +59,9 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { for (let i = 0, n = amount; i < n; i++) { rawArray.push(i) } - const arr = readonly(rawArray) + const arr = reactive(rawArray) - bench(`track forEach on readonly array, ${amount} elements`, () => { + bench(`track forEach, ${amount} elements`, () => { let sum = 0 effect(() => { arr.forEach(x => (sum += x)) @@ -127,9 +74,9 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { for (let i = 0, n = amount; i < n; i++) { rawArray.push(i) } - const arr = readonly(rawArray) + const arr = reactive(rawArray) - bench(`track reduce on readonly array, ${amount} elements`, () => { + bench(`track reduce, ${amount} elements`, () => { let sum = 0 effect(() => { sum = arr.reduce((v, a) => a + v, 0) @@ -146,7 +93,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { effect(() => r.reduce((v, a) => a + v, 0)) bench( - `trigger index mutation (1st only) on *reactive* array (tracked with reduce), ${amount} elements`, + `trigger index mutation (1st only), tracked with reduce, ${amount} elements`, () => { r[0]++ }, @@ -162,7 +109,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { effect(() => r.reduce((v, a) => a + v, 0)) bench( - `trigger index mutation (all) on *reactive* array (tracked with reduce), ${amount} elements`, + `trigger index mutation (all), tracked with reduce, ${amount} elements`, () => { for (let i = 0, n = r.length; i < n; i++) { r[i]++ @@ -184,12 +131,9 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { } }) - bench( - `push() trigger on reactive array tracked via iteration, ${amount} elements`, - () => { - arr.push(1) - }, - ) + bench(`push() trigger, tracked via iteration, ${amount} elements`, () => { + arr.push(1) + }) } { @@ -203,11 +147,8 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { arr.forEach(x => (sum += x)) }) - bench( - `push() trigger on reactive array tracked via forEach, ${amount} elements`, - () => { - arr.push(1) - }, - ) + bench(`push() trigger, tracked via forEach, ${amount} elements`, () => { + arr.push(1) + }) } } diff --git a/packages/reactivity/src/arrayInstrumentations.ts b/packages/reactivity/src/arrayInstrumentations.ts index d1df1052be5..a16eabdf72f 100644 --- a/packages/reactivity/src/arrayInstrumentations.ts +++ b/packages/reactivity/src/arrayInstrumentations.ts @@ -3,6 +3,11 @@ 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(array: T[]): T[] { const raw = toRaw(array) if (raw === array) return raw @@ -10,7 +15,10 @@ export function reactiveReadArray(array: T[]): T[] { return isShallow(array) ? raw : raw.map(toReactive) } -function shallowReadArray(arr: T[]): T[] { +/** + * Track array iteration and return raw array + */ +export function shallowReadArray(arr: T[]): T[] { track((arr = toRaw(arr)), TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY) return arr } diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 364f5e43e04..e372f4403d0 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -73,5 +73,5 @@ export { getCurrentScope, onScopeDispose, } from './effectScope' -export { reactiveReadArray } from './arrayInstrumentations' +export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations' export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants' From 13d31d573939cd5042ca7fd7d4809f4ec189b6aa Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 26 Feb 2024 17:37:55 +0800 Subject: [PATCH 6/6] perf: avoid array map in v-for read array --- .../__benchmarks__/reactiveArray.bench.ts | 2 +- packages/reactivity/src/index.ts | 2 ++ packages/runtime-core/src/helpers/renderList.ts | 14 ++++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/reactivity/__benchmarks__/reactiveArray.bench.ts b/packages/reactivity/__benchmarks__/reactiveArray.bench.ts index 93fa61501de..f5032cf7ae9 100644 --- a/packages/reactivity/__benchmarks__/reactiveArray.bench.ts +++ b/packages/reactivity/__benchmarks__/reactiveArray.bench.ts @@ -1,5 +1,5 @@ import { bench } from 'vitest' -import { effect, reactive, reactiveReadArray, shallowReadArray } from '../src' +import { effect, reactive, shallowReadArray } from '../src' for (let amount = 1e1; amount < 1e4; amount *= 10) { { diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index e372f4403d0..609afc05f8a 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -31,6 +31,8 @@ export { shallowReadonly, markRaw, toRaw, + toReactive, + toReadonly, type Raw, type DeepReadonly, type ShallowReactive, diff --git a/packages/runtime-core/src/helpers/renderList.ts b/packages/runtime-core/src/helpers/renderList.ts index f5f27542c97..0abb68aef9c 100644 --- a/packages/runtime-core/src/helpers/renderList.ts +++ b/packages/runtime-core/src/helpers/renderList.ts @@ -1,5 +1,5 @@ import type { VNode, VNodeChild } from '../vnode' -import { reactiveReadArray } from '@vue/reactivity' +import { isReactive, shallowReadArray, toReactive } from '@vue/reactivity' import { isArray, isObject, isString } from '@vue/shared' import { warn } from '../warning' @@ -60,14 +60,20 @@ export function renderList( let ret: VNodeChild[] const cached = (cache && cache[index!]) as VNode[] | undefined const sourceIsArray = isArray(source) + const sourceIsReactiveArray = sourceIsArray && isReactive(source) if (sourceIsArray || isString(source)) { - if (sourceIsArray) { - source = reactiveReadArray(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)) {