mirror of https://github.com/vuejs/core.git
parent
72bde94e66
commit
70196a40cc
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Reference in New Issue