mirror of https://github.com/vuejs/core.git
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
174118ae40
|
|
@ -0,0 +1,200 @@
|
|||
import { bench, describe } from 'vitest'
|
||||
import { type ComputedRef, type Ref, computed, effect, ref } from '../src'
|
||||
|
||||
describe('computed', () => {
|
||||
bench('create computed', () => {
|
||||
computed(() => 100)
|
||||
})
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
computed(() => v.value * 2)
|
||||
let i = 0
|
||||
bench("write ref, don't read computed (without effect)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
effect(() => c.value)
|
||||
let i = 0
|
||||
bench("write ref, don't read computed (with effect)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
let i = 0
|
||||
bench('write ref, read computed (without effect)', () => {
|
||||
v.value = i++
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
effect(() => c.value)
|
||||
let i = 0
|
||||
bench('write ref, read computed (with effect)', () => {
|
||||
v.value = i++
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds: ComputedRef<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench("write ref, don't read 1000 computeds (without effect)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds: ComputedRef<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
effect(() => c.value)
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench(
|
||||
"write ref, don't read 1000 computeds (with multiple effects)",
|
||||
() => {
|
||||
v.value = i++
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds: ComputedRef<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
computeds.push(c)
|
||||
}
|
||||
effect(() => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
computeds[i].value
|
||||
}
|
||||
})
|
||||
let i = 0
|
||||
bench("write ref, don't read 1000 computeds (with single effect)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds: ComputedRef<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench('write ref, read 1000 computeds (no effect)', () => {
|
||||
v.value = i++
|
||||
computeds.forEach(c => c.value)
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds: ComputedRef<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
effect(() => c.value)
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench('write ref, read 1000 computeds (with multiple effects)', () => {
|
||||
v.value = i++
|
||||
computeds.forEach(c => c.value)
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds: ComputedRef<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
effect(() => c.value)
|
||||
computeds.push(c)
|
||||
}
|
||||
effect(() => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
computeds[i].value
|
||||
}
|
||||
})
|
||||
let i = 0
|
||||
bench('write ref, read 1000 computeds (with single effect)', () => {
|
||||
v.value = i++
|
||||
computeds.forEach(c => c.value)
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const refs: Ref<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
refs.push(ref(i))
|
||||
}
|
||||
const c = computed(() => {
|
||||
let total = 0
|
||||
refs.forEach(ref => (total += ref.value))
|
||||
return total
|
||||
})
|
||||
let i = 0
|
||||
const n = refs.length
|
||||
bench('1000 refs, read 1 computed (without effect)', () => {
|
||||
refs[i++ % n].value++
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const refs: Ref<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
refs.push(ref(i))
|
||||
}
|
||||
const c = computed(() => {
|
||||
let total = 0
|
||||
refs.forEach(ref => (total += ref.value))
|
||||
return total
|
||||
})
|
||||
effect(() => c.value)
|
||||
let i = 0
|
||||
const n = refs.length
|
||||
bench('1000 refs, read 1 computed (with effect)', () => {
|
||||
refs[i++ % n].value++
|
||||
c.value
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import { bench, describe } from 'vitest'
|
||||
import { type Ref, effect, ref } from '../src'
|
||||
|
||||
describe('effect', () => {
|
||||
{
|
||||
let i = 0
|
||||
const n = ref(0)
|
||||
effect(() => n.value)
|
||||
bench('single ref invoke', () => {
|
||||
n.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
function benchEffectCreate(size: number) {
|
||||
bench(`create an effect that tracks ${size} refs`, () => {
|
||||
const refs: Ref[] = []
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs.push(ref(i))
|
||||
}
|
||||
effect(() => {
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs[i].value
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
benchEffectCreate(1)
|
||||
benchEffectCreate(10)
|
||||
benchEffectCreate(100)
|
||||
benchEffectCreate(1000)
|
||||
|
||||
function benchEffectCreateAndStop(size: number) {
|
||||
bench(`create and stop an effect that tracks ${size} refs`, () => {
|
||||
const refs: Ref[] = []
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs.push(ref(i))
|
||||
}
|
||||
const e = effect(() => {
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs[i].value
|
||||
}
|
||||
})
|
||||
e.effect.stop()
|
||||
})
|
||||
}
|
||||
|
||||
benchEffectCreateAndStop(1)
|
||||
benchEffectCreateAndStop(10)
|
||||
benchEffectCreateAndStop(100)
|
||||
benchEffectCreateAndStop(1000)
|
||||
|
||||
function benchWithRefs(size: number) {
|
||||
let j = 0
|
||||
const refs: Ref[] = []
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs.push(ref(i))
|
||||
}
|
||||
effect(() => {
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs[i].value
|
||||
}
|
||||
})
|
||||
bench(`1 effect, mutate ${size} refs`, () => {
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs[i].value = i + j++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
benchWithRefs(10)
|
||||
benchWithRefs(100)
|
||||
benchWithRefs(1000)
|
||||
|
||||
function benchWithBranches(size: number) {
|
||||
const toggle = ref(true)
|
||||
const refs: Ref[] = []
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs.push(ref(i))
|
||||
}
|
||||
effect(() => {
|
||||
if (toggle.value) {
|
||||
for (let i = 0; i < size; i++) {
|
||||
refs[i].value
|
||||
}
|
||||
}
|
||||
})
|
||||
bench(`${size} refs branch toggle`, () => {
|
||||
toggle.value = !toggle.value
|
||||
})
|
||||
}
|
||||
|
||||
benchWithBranches(10)
|
||||
benchWithBranches(100)
|
||||
benchWithBranches(1000)
|
||||
|
||||
function benchMultipleEffects(size: number) {
|
||||
let i = 0
|
||||
const n = ref(0)
|
||||
for (let i = 0; i < size; i++) {
|
||||
effect(() => n.value)
|
||||
}
|
||||
bench(`1 ref invoking ${size} effects`, () => {
|
||||
n.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
benchMultipleEffects(10)
|
||||
benchMultipleEffects(100)
|
||||
benchMultipleEffects(1000)
|
||||
})
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import { bench } from 'vitest'
|
||||
import { effect, reactive, shallowReadArray } from '../src'
|
||||
|
||||
for (let amount = 1e1; amount < 1e4; amount *= 10) {
|
||||
{
|
||||
const rawArray: number[] = []
|
||||
for (let i = 0, n = amount; i < n; i++) {
|
||||
rawArray.push(i)
|
||||
}
|
||||
const arr = reactive(rawArray)
|
||||
|
||||
bench(`track for loop, ${amount} elements`, () => {
|
||||
let sum = 0
|
||||
effect(() => {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
sum += arr[i]
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const rawArray: any[] = []
|
||||
for (let i = 0, n = amount; i < n; i++) {
|
||||
rawArray.push(i)
|
||||
}
|
||||
const r = reactive(rawArray)
|
||||
effect(() => r.reduce((v, a) => a + v, 0))
|
||||
|
||||
bench(
|
||||
`trigger index mutation (1st only), tracked with reduce, ${amount} elements`,
|
||||
() => {
|
||||
r[0]++
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
const rawArray: any[] = []
|
||||
for (let i = 0, n = amount; i < n; i++) {
|
||||
rawArray.push(i)
|
||||
}
|
||||
const r = reactive(rawArray)
|
||||
effect(() => r.reduce((v, a) => a + v, 0))
|
||||
|
||||
bench(
|
||||
`trigger index mutation (all), tracked with reduce, ${amount} elements`,
|
||||
() => {
|
||||
for (let i = 0, n = r.length; i < n; i++) {
|
||||
r[i]++
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
const rawArray: number[] = []
|
||||
for (let i = 0, n = amount; i < n; i++) {
|
||||
rawArray.push(i)
|
||||
}
|
||||
const arr = reactive(rawArray)
|
||||
let sum = 0
|
||||
effect(() => {
|
||||
for (let x of arr) {
|
||||
sum += x
|
||||
}
|
||||
})
|
||||
|
||||
bench(`push() trigger, tracked via iteration, ${amount} elements`, () => {
|
||||
arr.push(1)
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const rawArray: number[] = []
|
||||
for (let i = 0, n = amount; i < n; i++) {
|
||||
rawArray.push(i)
|
||||
}
|
||||
const arr = reactive(rawArray)
|
||||
let sum = 0
|
||||
effect(() => {
|
||||
arr.forEach(x => (sum += x))
|
||||
})
|
||||
|
||||
bench(`push() trigger, tracked via forEach, ${amount} elements`, () => {
|
||||
arr.push(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ bench('create reactive map', () => {
|
|||
|
||||
{
|
||||
const r = reactive(createMap({ a: 1 }))
|
||||
const computeds = []
|
||||
const computeds: any[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return r.get('a') * 2
|
||||
|
|
@ -94,7 +94,7 @@ bench('create reactive map', () => {
|
|||
|
||||
{
|
||||
const r = reactive(createMap({ a: 1 }))
|
||||
const computeds = []
|
||||
const computeds: any[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return r.get('a') * 2
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { bench } from 'vitest'
|
||||
import { reactive } from '../src'
|
||||
|
||||
bench('create reactive obj', () => {
|
||||
reactive({ a: 1 })
|
||||
})
|
||||
|
||||
{
|
||||
const r = reactive({ a: 1 })
|
||||
bench('read reactive obj property', () => {
|
||||
r.a
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
let i = 0
|
||||
const r = reactive({ a: 1 })
|
||||
bench('write reactive obj property', () => {
|
||||
r.a = i++
|
||||
})
|
||||
}
|
||||
|
|
@ -26,7 +26,6 @@ describe('ref', () => {
|
|||
const v = ref(100)
|
||||
bench('write/read ref', () => {
|
||||
v.value = i++
|
||||
|
||||
v.value
|
||||
})
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
import { bench, describe } from 'vitest'
|
||||
import { type ComputedRef, type Ref, computed, ref } from '../src/index'
|
||||
|
||||
describe('computed', () => {
|
||||
bench('create computed', () => {
|
||||
computed(() => 100)
|
||||
})
|
||||
|
||||
{
|
||||
let i = 0
|
||||
const o = ref(100)
|
||||
bench('write independent ref dep', () => {
|
||||
o.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
computed(() => v.value * 2)
|
||||
let i = 0
|
||||
bench("write ref, don't read computed (never invoked)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
let i = 0
|
||||
bench("write ref, don't read computed (never invoked)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
c.value
|
||||
let i = 0
|
||||
bench("write ref, don't read computed (invoked)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
let i = 0
|
||||
bench('write ref, read computed', () => {
|
||||
v.value = i++
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench("write ref, don't read 1000 computeds (never invoked)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
c.value
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench("write ref, don't read 1000 computeds (invoked)", () => {
|
||||
v.value = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const v = ref(100)
|
||||
const computeds: ComputedRef<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return v.value * 2
|
||||
})
|
||||
c.value
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench('write ref, read 1000 computeds', () => {
|
||||
v.value = i++
|
||||
computeds.forEach(c => c.value)
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const refs: Ref<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
refs.push(ref(i))
|
||||
}
|
||||
const c = computed(() => {
|
||||
let total = 0
|
||||
refs.forEach(ref => (total += ref.value))
|
||||
return total
|
||||
})
|
||||
let i = 0
|
||||
const n = refs.length
|
||||
bench('1000 refs, 1 computed', () => {
|
||||
refs[i++ % n].value++
|
||||
c.value
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -1,4 +1,12 @@
|
|||
import { h, nextTick, nodeOps, render, serializeInner } from '@vue/runtime-test'
|
||||
import {
|
||||
h,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
render,
|
||||
serializeInner,
|
||||
} from '@vue/runtime-test'
|
||||
import {
|
||||
type DebuggerEvent,
|
||||
ITERATE_KEY,
|
||||
|
|
@ -13,8 +21,8 @@ import {
|
|||
shallowRef,
|
||||
toRaw,
|
||||
} from '../src'
|
||||
import { DirtyLevels } from '../src/constants'
|
||||
import { COMPUTED_SIDE_EFFECT_WARN } from '../src/computed'
|
||||
import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
|
||||
import type { ComputedRef, ComputedRefImpl } from '../src/computed'
|
||||
|
||||
describe('reactivity/computed', () => {
|
||||
it('should return updated value', () => {
|
||||
|
|
@ -123,21 +131,6 @@ describe('reactivity/computed', () => {
|
|||
expect(getter2).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should no longer update when stopped', () => {
|
||||
const value = reactive<{ foo?: number }>({})
|
||||
const cValue = computed(() => value.foo)
|
||||
let dummy
|
||||
effect(() => {
|
||||
dummy = cValue.value
|
||||
})
|
||||
expect(dummy).toBe(undefined)
|
||||
value.foo = 1
|
||||
expect(dummy).toBe(1)
|
||||
cValue.effect.stop()
|
||||
value.foo = 2
|
||||
expect(dummy).toBe(1)
|
||||
})
|
||||
|
||||
it('should support setter', () => {
|
||||
const n = ref(1)
|
||||
const plusOne = computed({
|
||||
|
|
@ -219,12 +212,6 @@ describe('reactivity/computed', () => {
|
|||
expect(isReadonly(z.value.a)).toBe(false)
|
||||
})
|
||||
|
||||
it('should expose value when stopped', () => {
|
||||
const x = computed(() => 1)
|
||||
x.effect.stop()
|
||||
expect(x.value).toBe(1)
|
||||
})
|
||||
|
||||
it('debug: onTrack', () => {
|
||||
let events: DebuggerEvent[] = []
|
||||
const onTrack = vi.fn((e: DebuggerEvent) => {
|
||||
|
|
@ -238,19 +225,19 @@ describe('reactivity/computed', () => {
|
|||
expect(onTrack).toHaveBeenCalledTimes(3)
|
||||
expect(events).toEqual([
|
||||
{
|
||||
effect: c.effect,
|
||||
effect: c,
|
||||
target: toRaw(obj),
|
||||
type: TrackOpTypes.GET,
|
||||
key: 'foo',
|
||||
},
|
||||
{
|
||||
effect: c.effect,
|
||||
effect: c,
|
||||
target: toRaw(obj),
|
||||
type: TrackOpTypes.HAS,
|
||||
key: 'bar',
|
||||
},
|
||||
{
|
||||
effect: c.effect,
|
||||
effect: c,
|
||||
target: toRaw(obj),
|
||||
type: TrackOpTypes.ITERATE,
|
||||
key: ITERATE_KEY,
|
||||
|
|
@ -266,14 +253,14 @@ describe('reactivity/computed', () => {
|
|||
const obj = reactive<{ foo?: number }>({ foo: 1 })
|
||||
const c = computed(() => obj.foo, { onTrigger })
|
||||
|
||||
// computed won't trigger compute until accessed
|
||||
c.value
|
||||
// computed won't track until it has a subscriber
|
||||
effect(() => c.value)
|
||||
|
||||
obj.foo!++
|
||||
expect(c.value).toBe(2)
|
||||
expect(onTrigger).toHaveBeenCalledTimes(1)
|
||||
expect(events[0]).toEqual({
|
||||
effect: c.effect,
|
||||
effect: c,
|
||||
target: toRaw(obj),
|
||||
type: TriggerOpTypes.SET,
|
||||
key: 'foo',
|
||||
|
|
@ -285,7 +272,7 @@ describe('reactivity/computed', () => {
|
|||
expect(c.value).toBeUndefined()
|
||||
expect(onTrigger).toHaveBeenCalledTimes(2)
|
||||
expect(events[1]).toEqual({
|
||||
effect: c.effect,
|
||||
effect: c,
|
||||
target: toRaw(obj),
|
||||
type: TriggerOpTypes.DELETE,
|
||||
key: 'foo',
|
||||
|
|
@ -380,17 +367,17 @@ describe('reactivity/computed', () => {
|
|||
const a = ref(0)
|
||||
const b = computed(() => {
|
||||
return a.value % 3 !== 0
|
||||
})
|
||||
}) as unknown as ComputedRefImpl
|
||||
const c = computed(() => {
|
||||
cSpy()
|
||||
if (a.value % 3 === 2) {
|
||||
return 'expensive'
|
||||
}
|
||||
return 'cheap'
|
||||
})
|
||||
}) as unknown as ComputedRefImpl
|
||||
const d = computed(() => {
|
||||
return a.value % 3 === 2
|
||||
})
|
||||
}) as unknown as ComputedRefImpl
|
||||
const e = computed(() => {
|
||||
if (b.value) {
|
||||
if (d.value) {
|
||||
|
|
@ -398,16 +385,15 @@ describe('reactivity/computed', () => {
|
|||
}
|
||||
}
|
||||
return c.value
|
||||
})
|
||||
}) as unknown as ComputedRefImpl
|
||||
|
||||
e.value
|
||||
a.value++
|
||||
e.value
|
||||
|
||||
expect(e.effect.deps.length).toBe(3)
|
||||
expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
|
||||
expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
|
||||
expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
|
||||
expect(e.deps!.dep).toBe(b.dep)
|
||||
expect(e.deps!.nextDep!.dep).toBe(d.dep)
|
||||
expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
|
||||
expect(cSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
a.value++
|
||||
|
|
@ -456,17 +442,14 @@ describe('reactivity/computed', () => {
|
|||
expect(fnSpy).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should chained recurse effects clear dirty after trigger', () => {
|
||||
it('should chained recursive effects clear dirty after trigger', () => {
|
||||
const v = ref(1)
|
||||
const c1 = computed(() => v.value)
|
||||
const c2 = computed(() => c1.value)
|
||||
const c1 = computed(() => v.value) as unknown as ComputedRefImpl
|
||||
const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
|
||||
|
||||
c1.effect.allowRecurse = true
|
||||
c2.effect.allowRecurse = true
|
||||
c2.value
|
||||
|
||||
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
|
||||
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
|
||||
expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
|
||||
expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should chained computeds dirtyLevel update with first computed effect', () => {
|
||||
|
|
@ -481,15 +464,7 @@ describe('reactivity/computed', () => {
|
|||
const c3 = computed(() => c2.value)
|
||||
|
||||
c3.value
|
||||
|
||||
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
|
||||
expect(c2.effect._dirtyLevel).toBe(
|
||||
DirtyLevels.MaybeDirty_ComputedSideEffect,
|
||||
)
|
||||
expect(c3.effect._dirtyLevel).toBe(
|
||||
DirtyLevels.MaybeDirty_ComputedSideEffect,
|
||||
)
|
||||
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('should work when chained(ref+computed)', () => {
|
||||
|
|
@ -502,9 +477,8 @@ describe('reactivity/computed', () => {
|
|||
})
|
||||
const c2 = computed(() => v.value + c1.value)
|
||||
expect(c2.value).toBe('0foo')
|
||||
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
|
||||
expect(c2.value).toBe('1foo')
|
||||
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('should trigger effect even computed already dirty', () => {
|
||||
|
|
@ -519,15 +493,16 @@ describe('reactivity/computed', () => {
|
|||
const c2 = computed(() => v.value + c1.value)
|
||||
|
||||
effect(() => {
|
||||
fnSpy()
|
||||
c2.value
|
||||
fnSpy(c2.value)
|
||||
})
|
||||
expect(fnSpy).toBeCalledTimes(1)
|
||||
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
|
||||
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
|
||||
expect(fnSpy.mock.calls).toMatchObject([['0foo']])
|
||||
expect(v.value).toBe(1)
|
||||
v.value = 2
|
||||
expect(fnSpy).toBeCalledTimes(2)
|
||||
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
expect(fnSpy.mock.calls).toMatchObject([['0foo'], ['2foo']])
|
||||
expect(v.value).toBe(2)
|
||||
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
// #10185
|
||||
|
|
@ -553,25 +528,12 @@ describe('reactivity/computed', () => {
|
|||
|
||||
c3.value
|
||||
v2.value = true
|
||||
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
|
||||
expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
|
||||
|
||||
c3.value
|
||||
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
|
||||
expect(c2.effect._dirtyLevel).toBe(
|
||||
DirtyLevels.MaybeDirty_ComputedSideEffect,
|
||||
)
|
||||
expect(c3.effect._dirtyLevel).toBe(
|
||||
DirtyLevels.MaybeDirty_ComputedSideEffect,
|
||||
)
|
||||
|
||||
v1.value.v.value = 999
|
||||
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
|
||||
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
|
||||
expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
|
||||
|
||||
expect(c3.value).toBe('yes')
|
||||
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
|
||||
|
|
@ -593,10 +555,10 @@ describe('reactivity/computed', () => {
|
|||
await nextTick()
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`2`)
|
||||
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('should not trigger effect scheduler by recurse computed effect', async () => {
|
||||
it('should not trigger effect scheduler by recursive computed effect', async () => {
|
||||
const v = ref('Hello')
|
||||
const c = computed(() => {
|
||||
v.value += ' World'
|
||||
|
|
@ -615,7 +577,279 @@ describe('reactivity/computed', () => {
|
|||
|
||||
v.value += ' World'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('Hello World World World World')
|
||||
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
expect(serializeInner(root)).toBe('Hello World World World')
|
||||
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('should not trigger if value did not change', () => {
|
||||
const src = ref(0)
|
||||
const c = computed(() => src.value % 2)
|
||||
const spy = vi.fn()
|
||||
effect(() => {
|
||||
spy(c.value)
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
src.value = 2
|
||||
|
||||
// should not trigger
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 3
|
||||
src.value = 5
|
||||
// should trigger because latest value changes
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('chained computed trigger', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
effectSpy(c2.value)
|
||||
})
|
||||
|
||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(1)
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 1
|
||||
expect(c1Spy).toHaveBeenCalledTimes(2)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('chained computed avoid re-compute', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
effectSpy(c2.value)
|
||||
})
|
||||
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
src.value = 2
|
||||
src.value = 4
|
||||
src.value = 6
|
||||
expect(c1Spy).toHaveBeenCalledTimes(4)
|
||||
// c2 should not have to re-compute because c1 did not change.
|
||||
expect(c2Spy).toHaveBeenCalledTimes(1)
|
||||
// effect should not trigger because c2 did not change.
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('chained computed value invalidation', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
effectSpy(c2.value)
|
||||
})
|
||||
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(effectSpy).toHaveBeenCalledWith(1)
|
||||
expect(c2.value).toBe(1)
|
||||
|
||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 1
|
||||
// value should be available sync
|
||||
expect(c2.value).toBe(2)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('sync access of invalidated chained computed should not prevent final effect from running', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
effectSpy(c2.value)
|
||||
})
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 1
|
||||
// sync access c2
|
||||
c2.value
|
||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('computed should force track in untracked zone', () => {
|
||||
const n = ref(0)
|
||||
const spy1 = vi.fn()
|
||||
const spy2 = vi.fn()
|
||||
|
||||
let c: ComputedRef
|
||||
effect(() => {
|
||||
spy1()
|
||||
pauseTracking()
|
||||
n.value
|
||||
c = computed(() => n.value + 1)
|
||||
// access computed now to force refresh
|
||||
c.value
|
||||
effect(() => spy2(c.value))
|
||||
n.value
|
||||
resetTracking()
|
||||
})
|
||||
|
||||
expect(spy1).toHaveBeenCalledTimes(1)
|
||||
expect(spy2).toHaveBeenCalledTimes(1)
|
||||
|
||||
n.value++
|
||||
// outer effect should not trigger
|
||||
expect(spy1).toHaveBeenCalledTimes(1)
|
||||
// inner effect should trigger
|
||||
expect(spy2).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
// not recommended behavior, but needed for backwards compatibility
|
||||
// used in VueUse asyncComputed
|
||||
it('computed side effect should be able trigger', () => {
|
||||
const a = ref(false)
|
||||
const b = ref(false)
|
||||
const c = computed(() => {
|
||||
a.value = true
|
||||
return b.value
|
||||
})
|
||||
effect(() => {
|
||||
if (a.value) {
|
||||
b.value = true
|
||||
}
|
||||
})
|
||||
expect(b.value).toBe(false)
|
||||
// accessing c triggers change
|
||||
c.value
|
||||
expect(b.value).toBe(true)
|
||||
expect(c.value).toBe(true)
|
||||
})
|
||||
|
||||
it('chained computed should work when accessed before having subs', () => {
|
||||
const n = ref(0)
|
||||
const c = computed(() => n.value)
|
||||
const d = computed(() => c.value + 1)
|
||||
const spy = vi.fn()
|
||||
|
||||
// access
|
||||
d.value
|
||||
|
||||
let dummy
|
||||
effect(() => {
|
||||
spy()
|
||||
dummy = d.value
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(dummy).toBe(1)
|
||||
|
||||
n.value++
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
expect(dummy).toBe(2)
|
||||
})
|
||||
|
||||
// #10236
|
||||
it('chained computed should still refresh after owner component unmount', async () => {
|
||||
const a = ref(0)
|
||||
const spy = vi.fn()
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
const b = computed(() => a.value + 1)
|
||||
const c = computed(() => b.value + 1)
|
||||
// access
|
||||
c.value
|
||||
onUnmounted(() => spy(c.value))
|
||||
return () => {}
|
||||
},
|
||||
}
|
||||
|
||||
const show = ref(true)
|
||||
const Parent = {
|
||||
setup() {
|
||||
return () => (show.value ? h(Child) : null)
|
||||
},
|
||||
}
|
||||
|
||||
render(h(Parent), nodeOps.createElement('div'))
|
||||
|
||||
a.value++
|
||||
show.value = false
|
||||
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledWith(3)
|
||||
})
|
||||
|
||||
// case: radix-vue `useForwardExpose` sets a template ref during mount,
|
||||
// and checks for the element's closest form element in a computed.
|
||||
// the computed is expected to only evaluate after mount.
|
||||
it('computed deps should only be refreshed when the subscribing effect is run, not when scheduled', async () => {
|
||||
const calls: string[] = []
|
||||
const a = ref(0)
|
||||
const b = computed(() => {
|
||||
calls.push('b eval')
|
||||
return a.value + 1
|
||||
})
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
calls.push('mounted')
|
||||
})
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
ref: () => (a.value = 1),
|
||||
},
|
||||
b.value,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
render(h(App), nodeOps.createElement('div'))
|
||||
|
||||
await nextTick()
|
||||
expect(calls).toMatchObject(['b eval', 'mounted', 'b eval'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,155 +0,0 @@
|
|||
import { computed, effect, ref } from '../src'
|
||||
|
||||
describe('deferred computed', () => {
|
||||
test('should not trigger if value did not change', () => {
|
||||
const src = ref(0)
|
||||
const c = computed(() => src.value % 2)
|
||||
const spy = vi.fn()
|
||||
effect(() => {
|
||||
spy(c.value)
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
src.value = 2
|
||||
|
||||
// should not trigger
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 3
|
||||
src.value = 5
|
||||
// should trigger because latest value changes
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('chained computed trigger', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
effectSpy(c2.value)
|
||||
})
|
||||
|
||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(1)
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 1
|
||||
expect(c1Spy).toHaveBeenCalledTimes(2)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('chained computed avoid re-compute', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
effectSpy(c2.value)
|
||||
})
|
||||
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
src.value = 2
|
||||
src.value = 4
|
||||
src.value = 6
|
||||
expect(c1Spy).toHaveBeenCalledTimes(4)
|
||||
// c2 should not have to re-compute because c1 did not change.
|
||||
expect(c2Spy).toHaveBeenCalledTimes(1)
|
||||
// effect should not trigger because c2 did not change.
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('chained computed value invalidation', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
effectSpy(c2.value)
|
||||
})
|
||||
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(effectSpy).toHaveBeenCalledWith(1)
|
||||
expect(c2.value).toBe(1)
|
||||
|
||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 1
|
||||
// value should be available sync
|
||||
expect(c2.value).toBe(2)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('sync access of invalidated chained computed should not prevent final effect from running', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
||||
effect(() => {
|
||||
effectSpy(c2.value)
|
||||
})
|
||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 1
|
||||
// sync access c2
|
||||
c2.value
|
||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('should not compute if deactivated before scheduler is called', () => {
|
||||
const c1Spy = vi.fn()
|
||||
const src = ref(0)
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
effect(() => c1.value)
|
||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
c1.effect.stop()
|
||||
// trigger
|
||||
src.value++
|
||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -11,8 +11,7 @@ import {
|
|||
stop,
|
||||
toRaw,
|
||||
} from '../src/index'
|
||||
import { pauseScheduling, resetScheduling } from '../src/effect'
|
||||
import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
|
||||
import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
|
||||
import {
|
||||
computed,
|
||||
h,
|
||||
|
|
@ -22,6 +21,13 @@ import {
|
|||
render,
|
||||
serializeInner,
|
||||
} from '@vue/runtime-test'
|
||||
import {
|
||||
endBatch,
|
||||
onEffectCleanup,
|
||||
pauseTracking,
|
||||
resetTracking,
|
||||
startBatch,
|
||||
} from '../src/effect'
|
||||
|
||||
describe('reactivity/effect', () => {
|
||||
it('should run the passed function once (wrapped by a effect)', () => {
|
||||
|
|
@ -698,18 +704,6 @@ describe('reactivity/effect', () => {
|
|||
expect(dummy).toBe(1)
|
||||
})
|
||||
|
||||
it('lazy', () => {
|
||||
const obj = reactive({ foo: 1 })
|
||||
let dummy
|
||||
const runner = effect(() => (dummy = obj.foo), { lazy: true })
|
||||
expect(dummy).toBe(undefined)
|
||||
|
||||
expect(runner()).toBe(1)
|
||||
expect(dummy).toBe(1)
|
||||
obj.foo = 2
|
||||
expect(dummy).toBe(2)
|
||||
})
|
||||
|
||||
it('scheduler', () => {
|
||||
let dummy
|
||||
let run: any
|
||||
|
|
@ -1005,7 +999,7 @@ describe('reactivity/effect', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('should be triggered once with pauseScheduling', () => {
|
||||
it('should be triggered once with batching', () => {
|
||||
const counter = reactive({ num: 0 })
|
||||
|
||||
const counterSpy = vi.fn(() => counter.num)
|
||||
|
|
@ -1013,10 +1007,10 @@ describe('reactivity/effect', () => {
|
|||
|
||||
counterSpy.mockClear()
|
||||
|
||||
pauseScheduling()
|
||||
startBatch()
|
||||
counter.num++
|
||||
counter.num++
|
||||
resetScheduling()
|
||||
endBatch()
|
||||
expect(counterSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
|
|
@ -1049,47 +1043,76 @@ describe('reactivity/effect', () => {
|
|||
expect(renderSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
describe('empty dep cleanup', () => {
|
||||
it('nested effect should force track in untracked zone', () => {
|
||||
const n = ref(0)
|
||||
const spy1 = vi.fn()
|
||||
const spy2 = vi.fn()
|
||||
|
||||
effect(() => {
|
||||
spy1()
|
||||
pauseTracking()
|
||||
n.value
|
||||
effect(() => {
|
||||
n.value
|
||||
spy2()
|
||||
})
|
||||
n.value
|
||||
resetTracking()
|
||||
})
|
||||
|
||||
expect(spy1).toHaveBeenCalledTimes(1)
|
||||
expect(spy2).toHaveBeenCalledTimes(1)
|
||||
|
||||
n.value++
|
||||
// outer effect should not trigger
|
||||
expect(spy1).toHaveBeenCalledTimes(1)
|
||||
// inner effect should trigger
|
||||
expect(spy2).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
describe('dep unsubscribe', () => {
|
||||
function getSubCount(dep: Dep | undefined) {
|
||||
let count = 0
|
||||
let sub = dep!.subs
|
||||
while (sub) {
|
||||
count++
|
||||
sub = sub.prevSub
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
it('should remove the dep when the effect is stopped', () => {
|
||||
const obj = reactive({ prop: 1 })
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
const runner = effect(() => obj.prop)
|
||||
const dep = getDepFromReactive(toRaw(obj), 'prop')
|
||||
expect(dep).toHaveLength(1)
|
||||
expect(getSubCount(dep)).toBe(1)
|
||||
obj.prop = 2
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(1)
|
||||
expect(getSubCount(dep)).toBe(1)
|
||||
stop(runner)
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
expect(getSubCount(dep)).toBe(0)
|
||||
obj.prop = 3
|
||||
runner()
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
expect(getSubCount(dep)).toBe(0)
|
||||
})
|
||||
|
||||
it('should only remove the dep when the last effect is stopped', () => {
|
||||
const obj = reactive({ prop: 1 })
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
const runner1 = effect(() => obj.prop)
|
||||
const dep = getDepFromReactive(toRaw(obj), 'prop')
|
||||
expect(dep).toHaveLength(1)
|
||||
expect(getSubCount(dep)).toBe(1)
|
||||
const runner2 = effect(() => obj.prop)
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(2)
|
||||
expect(getSubCount(dep)).toBe(2)
|
||||
obj.prop = 2
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(2)
|
||||
expect(getSubCount(dep)).toBe(2)
|
||||
stop(runner1)
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(1)
|
||||
expect(getSubCount(dep)).toBe(1)
|
||||
obj.prop = 3
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||
expect(dep).toHaveLength(1)
|
||||
expect(getSubCount(dep)).toBe(1)
|
||||
stop(runner2)
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
obj.prop = 4
|
||||
runner1()
|
||||
runner2()
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
expect(getSubCount(dep)).toBe(0)
|
||||
})
|
||||
|
||||
it('should remove the dep when it is no longer used by the effect', () => {
|
||||
|
|
@ -1098,18 +1121,53 @@ describe('reactivity/effect', () => {
|
|||
b: 2,
|
||||
c: 'a',
|
||||
})
|
||||
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||
effect(() => obj[obj.c])
|
||||
const depC = getDepFromReactive(toRaw(obj), 'c')
|
||||
expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
|
||||
expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
|
||||
expect(depC).toHaveLength(1)
|
||||
expect(getSubCount(getDepFromReactive(toRaw(obj), 'a'))).toBe(1)
|
||||
expect(getSubCount(depC)).toBe(1)
|
||||
obj.c = 'b'
|
||||
obj.a = 4
|
||||
expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
|
||||
expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
|
||||
expect(getSubCount(getDepFromReactive(toRaw(obj), 'b'))).toBe(1)
|
||||
expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
|
||||
expect(depC).toHaveLength(1)
|
||||
expect(getSubCount(depC)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('onEffectCleanup', () => {
|
||||
it('should get called correctly', async () => {
|
||||
const count = ref(0)
|
||||
const cleanupEffect = vi.fn()
|
||||
|
||||
const e = effect(() => {
|
||||
onEffectCleanup(cleanupEffect)
|
||||
count.value
|
||||
})
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(cleanupEffect).toHaveBeenCalledTimes(1)
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(cleanupEffect).toHaveBeenCalledTimes(2)
|
||||
|
||||
// call it on stop
|
||||
e.effect.stop()
|
||||
expect(cleanupEffect).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should warn if called without active effect', () => {
|
||||
onEffectCleanup(() => {})
|
||||
expect(
|
||||
`onEffectCleanup() was called when there was no active effect`,
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('should not warn without active effect when failSilently argument is passed', () => {
|
||||
onEffectCleanup(() => {}, true)
|
||||
expect(
|
||||
`onEffectCleanup() was called when there was no active effect`,
|
||||
).not.toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -247,16 +247,15 @@ describe('reactivity/effect/scope', () => {
|
|||
watchEffect(() => {
|
||||
watchEffectSpy()
|
||||
r.value
|
||||
c.value
|
||||
})
|
||||
})
|
||||
|
||||
c!.value // computed is lazy so trigger collection
|
||||
expect(computedSpy).toHaveBeenCalledTimes(1)
|
||||
expect(watchSpy).toHaveBeenCalledTimes(0)
|
||||
expect(watchEffectSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
r.value++
|
||||
c!.value
|
||||
await nextTick()
|
||||
expect(computedSpy).toHaveBeenCalledTimes(2)
|
||||
expect(watchSpy).toHaveBeenCalledTimes(1)
|
||||
|
|
@ -265,7 +264,6 @@ describe('reactivity/effect/scope', () => {
|
|||
scope.stop()
|
||||
|
||||
r.value++
|
||||
c!.value
|
||||
await nextTick()
|
||||
// should not trigger anymore
|
||||
expect(computedSpy).toHaveBeenCalledTimes(2)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
shallowRef as ref,
|
||||
toRaw,
|
||||
} from '../src/index'
|
||||
import { getDepFromReactive } from '../src/reactiveEffect'
|
||||
import { getDepFromReactive } from '../src/dep'
|
||||
|
||||
describe.skipIf(!global.gc)('reactivity/gc', () => {
|
||||
const gc = () => {
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
import { bench } from 'vitest'
|
||||
import { computed, reactive, readonly, shallowRef, triggerRef } from '../src'
|
||||
|
||||
for (let amount = 1e1; amount < 1e4; amount *= 10) {
|
||||
{
|
||||
const rawArray = []
|
||||
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)
|
||||
})
|
||||
|
||||
bench(`reduce *reactive* array, ${amount} elements`, () => {
|
||||
for (let i = 0, n = r.length; i < n; i++) {
|
||||
r[i]++
|
||||
}
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const rawArray = []
|
||||
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)
|
||||
})
|
||||
|
||||
bench(
|
||||
`reduce *reactive* array, ${amount} elements, only change first value`,
|
||||
() => {
|
||||
r[0]++
|
||||
c.value
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
const rawArray = []
|
||||
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)
|
||||
})
|
||||
|
||||
bench(`reduce *readonly* array, ${amount} elements`, () => {
|
||||
r.arr = r.arr.map(v => v + 1)
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const rawArray = []
|
||||
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)
|
||||
})
|
||||
|
||||
bench(`reduce *raw* array, copied, ${amount} elements`, () => {
|
||||
r.value = r.value.map(v => v + 1)
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
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)
|
||||
})
|
||||
|
||||
bench(`reduce *raw* array, manually triggered, ${amount} elements`, () => {
|
||||
for (let i = 0, n = rawArray.length; i < n; i++) {
|
||||
rawArray[i]++
|
||||
}
|
||||
triggerRef(r)
|
||||
c.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
import { bench } from 'vitest'
|
||||
import { type ComputedRef, computed, reactive } from '../src'
|
||||
|
||||
bench('create reactive obj', () => {
|
||||
reactive({ a: 1 })
|
||||
})
|
||||
|
||||
{
|
||||
let i = 0
|
||||
const r = reactive({ a: 1 })
|
||||
bench('write reactive obj property', () => {
|
||||
r.a = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const r = reactive({ a: 1 })
|
||||
computed(() => {
|
||||
return r.a * 2
|
||||
})
|
||||
let i = 0
|
||||
bench("write reactive obj, don't read computed (never invoked)", () => {
|
||||
r.a = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const r = reactive({ a: 1 })
|
||||
const c = computed(() => {
|
||||
return r.a * 2
|
||||
})
|
||||
c.value
|
||||
let i = 0
|
||||
bench("write reactive obj, don't read computed (invoked)", () => {
|
||||
r.a = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const r = reactive({ a: 1 })
|
||||
const c = computed(() => {
|
||||
return r.a * 2
|
||||
})
|
||||
let i = 0
|
||||
bench('write reactive obj, read computed', () => {
|
||||
r.a = i++
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const r = reactive({ a: 1 })
|
||||
const computeds = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return r.a * 2
|
||||
})
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench("write reactive obj, don't read 1000 computeds (never invoked)", () => {
|
||||
r.a = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const r = reactive({ a: 1 })
|
||||
const computeds = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return r.a * 2
|
||||
})
|
||||
c.value
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench("write reactive obj, don't read 1000 computeds (invoked)", () => {
|
||||
r.a = i++
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const r = reactive({ a: 1 })
|
||||
const computeds: ComputedRef<number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
const c = computed(() => {
|
||||
return r.a * 2
|
||||
})
|
||||
computeds.push(c)
|
||||
}
|
||||
let i = 0
|
||||
bench('write reactive obj, read 1000 computeds', () => {
|
||||
r.a = i++
|
||||
computeds.forEach(c => c.value)
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const reactives: Record<string, number>[] = []
|
||||
for (let i = 0, n = 1000; i < n; i++) {
|
||||
reactives.push(reactive({ a: i }))
|
||||
}
|
||||
const c = computed(() => {
|
||||
let total = 0
|
||||
reactives.forEach(r => (total += r.a))
|
||||
return total
|
||||
})
|
||||
let i = 0
|
||||
const n = reactives.length
|
||||
bench('1000 reactive objs, 1 computed', () => {
|
||||
reactives[i++ % n].a++
|
||||
c.value
|
||||
})
|
||||
}
|
||||
|
|
@ -409,7 +409,7 @@ describe('reactivity/readonly', () => {
|
|||
const eff = effect(() => {
|
||||
roArr.includes(2)
|
||||
})
|
||||
expect(eff.effect.deps.length).toBe(0)
|
||||
expect(eff.effect.deps).toBeUndefined()
|
||||
})
|
||||
|
||||
test('readonly should track and trigger if wrapping reactive original (collection)', () => {
|
||||
|
|
|
|||
|
|
@ -442,4 +442,15 @@ describe('reactivity/ref', () => {
|
|||
expect(a.value).toBe(rr)
|
||||
expect(a.value).not.toBe(r)
|
||||
})
|
||||
|
||||
test('should not trigger when setting the same raw object', () => {
|
||||
const obj = {}
|
||||
const r = ref(obj)
|
||||
const spy = vi.fn()
|
||||
effect(() => spy(r.value))
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
r.value = obj
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ describe('shallowReactive', () => {
|
|||
shallowArray.pop()
|
||||
expect(size).toBe(0)
|
||||
})
|
||||
|
||||
test('should not observe when iterating', () => {
|
||||
const shallowArray = shallowReactive<object[]>([])
|
||||
const a = {}
|
||||
|
|
|
|||
|
|
@ -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,14 +10,9 @@ import {
|
|||
shallowReadonlyMap,
|
||||
toRaw,
|
||||
} from './reactive'
|
||||
import { arrayInstrumentations } from './arrayInstrumentations'
|
||||
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import {
|
||||
pauseScheduling,
|
||||
pauseTracking,
|
||||
resetScheduling,
|
||||
resetTracking,
|
||||
} from './effect'
|
||||
import { ITERATE_KEY, track, trigger } from './reactiveEffect'
|
||||
import { ITERATE_KEY, track, trigger } from './dep'
|
||||
import {
|
||||
hasChanged,
|
||||
hasOwn,
|
||||
|
|
@ -43,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[]) {
|
||||
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)
|
||||
|
|
@ -125,15 +83,23 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
const res = Reflect.get(target, key, receiver)
|
||||
const res = Reflect.get(
|
||||
target,
|
||||
key,
|
||||
// if this is a proxy wrapping a ref, return methods using the raw ref
|
||||
// as receiver so that we don't have to call `toRaw` on the ref in all
|
||||
// its class methods
|
||||
isRef(target) ? target : receiver,
|
||||
)
|
||||
|
||||
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { toRaw, toReactive, toReadonly } from './reactive'
|
||||
import {
|
||||
ITERATE_KEY,
|
||||
MAP_KEY_ITERATE_KEY,
|
||||
track,
|
||||
trigger,
|
||||
} from './reactiveEffect'
|
||||
import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep'
|
||||
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import { type DebuggerOptions, ReactiveEffect } from './effect'
|
||||
import { type Ref, trackRefValue, triggerRefValue } from './ref'
|
||||
import { NOOP, hasChanged, isFunction } from '@vue/shared'
|
||||
import { toRaw } from './reactive'
|
||||
import type { Dep } from './dep'
|
||||
import { DirtyLevels, ReactiveFlags } from './constants'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import {
|
||||
type DebuggerEvent,
|
||||
type DebuggerOptions,
|
||||
EffectFlags,
|
||||
type Link,
|
||||
type Subscriber,
|
||||
activeSub,
|
||||
refreshComputed,
|
||||
} from './effect'
|
||||
import type { Ref } from './ref'
|
||||
import { warn } from './warning'
|
||||
import { Dep, globalVersion } from './dep'
|
||||
import { ReactiveFlags, TrackOpTypes } from './constants'
|
||||
|
||||
declare const ComputedRefSymbol: unique symbol
|
||||
|
||||
|
|
@ -14,7 +21,10 @@ export interface ComputedRef<T = any> extends WritableComputedRef<T> {
|
|||
}
|
||||
|
||||
export interface WritableComputedRef<T> extends Ref<T> {
|
||||
readonly effect: ReactiveEffect<T>
|
||||
/**
|
||||
* @deprecated computed no longer uses effect
|
||||
*/
|
||||
effect: ComputedRefImpl
|
||||
}
|
||||
|
||||
export type ComputedGetter<T> = (oldValue?: T) => T
|
||||
|
|
@ -25,81 +35,107 @@ export interface WritableComputedOptions<T> {
|
|||
set: ComputedSetter<T>
|
||||
}
|
||||
|
||||
export const COMPUTED_SIDE_EFFECT_WARN =
|
||||
`Computed is still dirty after getter evaluation,` +
|
||||
` likely because a computed is mutating its own dependency in its getter.` +
|
||||
` State mutations in computed getters should be avoided. ` +
|
||||
` Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free`
|
||||
/**
|
||||
* @private exported by @vue/reactivity for Vue core use, but not exported from
|
||||
* the main vue package
|
||||
*/
|
||||
export class ComputedRefImpl<T = any> implements Subscriber {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_value: any = undefined
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
readonly dep = new Dep(this)
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
readonly __v_isRef = true;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
readonly [ReactiveFlags.IS_READONLY]: boolean
|
||||
// A computed is also a subscriber that tracks other deps
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
deps?: Link = undefined
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
depsTail?: Link = undefined
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
flags = EffectFlags.DIRTY
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
globalVersion = globalVersion - 1
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isSSR: boolean
|
||||
// for backwards compat
|
||||
effect = this
|
||||
|
||||
export class ComputedRefImpl<T> {
|
||||
public dep?: Dep = undefined
|
||||
|
||||
private _value!: T
|
||||
public readonly effect: ReactiveEffect<T>
|
||||
|
||||
public readonly __v_isRef = true
|
||||
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
|
||||
|
||||
public _cacheable: boolean
|
||||
// dev only
|
||||
onTrack?: (event: DebuggerEvent) => void
|
||||
// dev only
|
||||
onTrigger?: (event: DebuggerEvent) => void
|
||||
|
||||
/**
|
||||
* Dev only
|
||||
* @internal
|
||||
*/
|
||||
_warnRecursive?: boolean
|
||||
|
||||
constructor(
|
||||
private getter: ComputedGetter<T>,
|
||||
private readonly _setter: ComputedSetter<T>,
|
||||
isReadonly: boolean,
|
||||
public fn: ComputedGetter<T>,
|
||||
private readonly setter: ComputedSetter<T> | undefined,
|
||||
isSSR: boolean,
|
||||
) {
|
||||
this.effect = new ReactiveEffect(
|
||||
() => getter(this._value),
|
||||
() =>
|
||||
triggerRefValue(
|
||||
this,
|
||||
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
|
||||
? DirtyLevels.MaybeDirty_ComputedSideEffect
|
||||
: DirtyLevels.MaybeDirty,
|
||||
),
|
||||
)
|
||||
this.effect.computed = this
|
||||
this.effect.active = this._cacheable = !isSSR
|
||||
this[ReactiveFlags.IS_READONLY] = isReadonly
|
||||
this.__v_isReadonly = !setter
|
||||
this.isSSR = isSSR
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
notify() {
|
||||
// avoid infinite self recursion
|
||||
if (activeSub !== this) {
|
||||
this.flags |= EffectFlags.DIRTY
|
||||
this.dep.notify()
|
||||
} else if (__DEV__) {
|
||||
// TODO warn
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
|
||||
const self = toRaw(this)
|
||||
if (
|
||||
(!self._cacheable || self.effect.dirty) &&
|
||||
hasChanged(self._value, (self._value = self.effect.run()!))
|
||||
) {
|
||||
triggerRefValue(self, DirtyLevels.Dirty)
|
||||
const link = __DEV__
|
||||
? this.dep.track({
|
||||
target: this,
|
||||
type: TrackOpTypes.GET,
|
||||
key: 'value',
|
||||
})
|
||||
: this.dep.track()
|
||||
refreshComputed(this)
|
||||
// sync version after evaluation
|
||||
if (link) {
|
||||
link.version = this.dep.version
|
||||
}
|
||||
trackRefValue(self)
|
||||
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
|
||||
if (__DEV__ && (__TEST__ || this._warnRecursive)) {
|
||||
warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter)
|
||||
}
|
||||
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
|
||||
return this._value
|
||||
}
|
||||
|
||||
set value(newValue) {
|
||||
if (this.setter) {
|
||||
this.setter(newValue)
|
||||
} else if (__DEV__) {
|
||||
warn('Write operation failed: computed value is readonly')
|
||||
}
|
||||
return self._value
|
||||
}
|
||||
|
||||
set value(newValue: T) {
|
||||
this._setter(newValue)
|
||||
}
|
||||
|
||||
// #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
|
||||
get _dirty() {
|
||||
return this.effect.dirty
|
||||
}
|
||||
|
||||
set _dirty(v) {
|
||||
this.effect.dirty = v
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -149,26 +185,20 @@ export function computed<T>(
|
|||
isSSR = false,
|
||||
) {
|
||||
let getter: ComputedGetter<T>
|
||||
let setter: ComputedSetter<T>
|
||||
let setter: ComputedSetter<T> | undefined
|
||||
|
||||
const onlyGetter = isFunction(getterOrOptions)
|
||||
if (onlyGetter) {
|
||||
if (isFunction(getterOrOptions)) {
|
||||
getter = getterOrOptions
|
||||
setter = __DEV__
|
||||
? () => {
|
||||
warn('Write operation failed: computed value is readonly')
|
||||
}
|
||||
: NOOP
|
||||
} else {
|
||||
getter = getterOrOptions.get
|
||||
setter = getterOrOptions.set
|
||||
}
|
||||
|
||||
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
|
||||
const cRef = new ComputedRefImpl(getter, setter, isSSR)
|
||||
|
||||
if (__DEV__ && debugOptions && !isSSR) {
|
||||
cRef.effect.onTrack = debugOptions.onTrack
|
||||
cRef.effect.onTrigger = debugOptions.onTrigger
|
||||
cRef.onTrack = debugOptions.onTrack
|
||||
cRef.onTrigger = debugOptions.onTrigger
|
||||
}
|
||||
|
||||
return cRef as any
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
import { computed } from './computed'
|
||||
|
||||
/**
|
||||
* @deprecated use `computed` instead. See #5912
|
||||
*/
|
||||
export const deferredComputed = computed
|
||||
|
|
@ -1,17 +1,310 @@
|
|||
import type { ReactiveEffect } from './effect'
|
||||
import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
|
||||
import type { ComputedRefImpl } from './computed'
|
||||
import { type TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import {
|
||||
type DebuggerEventExtraInfo,
|
||||
EffectFlags,
|
||||
type Link,
|
||||
activeSub,
|
||||
endBatch,
|
||||
shouldTrack,
|
||||
startBatch,
|
||||
} from './effect'
|
||||
|
||||
export type Dep = Map<ReactiveEffect, number> & {
|
||||
cleanup: () => void
|
||||
computed?: ComputedRefImpl<any>
|
||||
/**
|
||||
* Incremented every time a reactive change happens
|
||||
* This is used to give computed a fast path to avoid re-compute when nothing
|
||||
* has changed.
|
||||
*/
|
||||
export let globalVersion = 0
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class Dep {
|
||||
version = 0
|
||||
/**
|
||||
* Link between this dep and the current active effect
|
||||
*/
|
||||
activeLink?: Link = undefined
|
||||
/**
|
||||
* Doubly linked list representing the subscribing effects (tail)
|
||||
*/
|
||||
subs?: Link = undefined
|
||||
|
||||
constructor(public computed?: ComputedRefImpl) {}
|
||||
|
||||
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
|
||||
if (!activeSub || !shouldTrack) {
|
||||
return
|
||||
}
|
||||
|
||||
let link = this.activeLink
|
||||
if (link === undefined || link.sub !== activeSub) {
|
||||
link = this.activeLink = {
|
||||
dep: this,
|
||||
sub: activeSub,
|
||||
version: this.version,
|
||||
nextDep: undefined,
|
||||
prevDep: undefined,
|
||||
nextSub: undefined,
|
||||
prevSub: undefined,
|
||||
prevActiveLink: undefined,
|
||||
}
|
||||
|
||||
// add the link to the activeEffect as a dep (as tail)
|
||||
if (!activeSub.deps) {
|
||||
activeSub.deps = activeSub.depsTail = link
|
||||
} else {
|
||||
link.prevDep = activeSub.depsTail
|
||||
activeSub.depsTail!.nextDep = link
|
||||
activeSub.depsTail = link
|
||||
}
|
||||
|
||||
if (activeSub.flags & EffectFlags.TRACKING) {
|
||||
addSub(link)
|
||||
}
|
||||
} else if (link.version === -1) {
|
||||
// reused from last run - already a sub, just sync version
|
||||
link.version = this.version
|
||||
|
||||
// If this dep has a next, it means it's not at the tail - move it to the
|
||||
// tail. This ensures the effect's dep list is in the order they are
|
||||
// accessed during evaluation.
|
||||
if (link.nextDep) {
|
||||
const next = link.nextDep
|
||||
next.prevDep = link.prevDep
|
||||
if (link.prevDep) {
|
||||
link.prevDep.nextDep = next
|
||||
}
|
||||
|
||||
link.prevDep = activeSub.depsTail
|
||||
link.nextDep = undefined
|
||||
activeSub.depsTail!.nextDep = link
|
||||
activeSub.depsTail = link
|
||||
|
||||
// this was the head - point to the new head
|
||||
if (activeSub.deps === link) {
|
||||
activeSub.deps = next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__ && activeSub.onTrack) {
|
||||
activeSub.onTrack(
|
||||
extend(
|
||||
{
|
||||
effect: activeSub,
|
||||
},
|
||||
debugInfo,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return link
|
||||
}
|
||||
|
||||
trigger(debugInfo?: DebuggerEventExtraInfo) {
|
||||
this.version++
|
||||
globalVersion++
|
||||
this.notify(debugInfo)
|
||||
}
|
||||
|
||||
notify(debugInfo?: DebuggerEventExtraInfo) {
|
||||
startBatch()
|
||||
try {
|
||||
for (let link = this.subs; link; link = link.prevSub) {
|
||||
if (
|
||||
__DEV__ &&
|
||||
link.sub.onTrigger &&
|
||||
!(link.sub.flags & EffectFlags.NOTIFIED)
|
||||
) {
|
||||
link.sub.onTrigger(
|
||||
extend(
|
||||
{
|
||||
effect: link.sub,
|
||||
},
|
||||
debugInfo,
|
||||
),
|
||||
)
|
||||
}
|
||||
link.sub.notify()
|
||||
}
|
||||
} finally {
|
||||
endBatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createDep = (
|
||||
cleanup: () => void,
|
||||
computed?: ComputedRefImpl<any>,
|
||||
): Dep => {
|
||||
const dep = new Map() as Dep
|
||||
dep.cleanup = cleanup
|
||||
dep.computed = computed
|
||||
return dep
|
||||
function addSub(link: Link) {
|
||||
const computed = link.dep.computed
|
||||
// computed getting its first subscriber
|
||||
// enable tracking + lazily subscribe to all its deps
|
||||
if (computed && !link.dep.subs) {
|
||||
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
|
||||
for (let l = computed.deps; l; l = l.nextDep) {
|
||||
addSub(l)
|
||||
}
|
||||
}
|
||||
|
||||
const currentTail = link.dep.subs
|
||||
if (currentTail !== link) {
|
||||
link.prevSub = currentTail
|
||||
if (currentTail) currentTail.nextSub = link
|
||||
}
|
||||
link.dep.subs = link
|
||||
}
|
||||
|
||||
// The main WeakMap that stores {target -> key -> dep} connections.
|
||||
// Conceptually, it's easier to think of a dependency as a Dep class
|
||||
// which maintains a Set of subscribers, but we simply store them as
|
||||
// raw Maps to reduce memory overhead.
|
||||
type KeyToDepMap = Map<any, Dep>
|
||||
const targetMap = new WeakMap<object, KeyToDepMap>()
|
||||
|
||||
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.
|
||||
*
|
||||
* This will check which effect is running at the moment and record it as dep
|
||||
* which records all effects that depend on the reactive property.
|
||||
*
|
||||
* @param target - Object holding the reactive property.
|
||||
* @param type - Defines the type of access to the reactive property.
|
||||
* @param key - Identifier of the reactive property to track.
|
||||
*/
|
||||
export function track(target: object, type: TrackOpTypes, key: unknown) {
|
||||
if (shouldTrack && activeSub) {
|
||||
let depsMap = targetMap.get(target)
|
||||
if (!depsMap) {
|
||||
targetMap.set(target, (depsMap = new Map()))
|
||||
}
|
||||
let dep = depsMap.get(key)
|
||||
if (!dep) {
|
||||
depsMap.set(key, (dep = new Dep()))
|
||||
}
|
||||
if (__DEV__) {
|
||||
dep.track({
|
||||
target,
|
||||
type,
|
||||
key,
|
||||
})
|
||||
} else {
|
||||
dep.track()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all deps associated with the target (or a specific property) and
|
||||
* triggers the effects stored within.
|
||||
*
|
||||
* @param target - The reactive object.
|
||||
* @param type - Defines the type of the operation that needs to trigger effects.
|
||||
* @param key - Can be used to target a specific reactive property in the target object.
|
||||
*/
|
||||
export function trigger(
|
||||
target: object,
|
||||
type: TriggerOpTypes,
|
||||
key?: unknown,
|
||||
newValue?: unknown,
|
||||
oldValue?: unknown,
|
||||
oldTarget?: Map<unknown, unknown> | Set<unknown>,
|
||||
) {
|
||||
const depsMap = targetMap.get(target)
|
||||
if (!depsMap) {
|
||||
// never been tracked
|
||||
globalVersion++
|
||||
return
|
||||
}
|
||||
|
||||
let deps: Dep[] = []
|
||||
if (type === TriggerOpTypes.CLEAR) {
|
||||
// collection being cleared
|
||||
// trigger all effects for target
|
||||
deps = [...depsMap.values()]
|
||||
} else {
|
||||
const targetIsArray = isArray(target)
|
||||
const isArrayIndex = targetIsArray && isIntegerKey(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 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'))
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startBatch()
|
||||
for (const dep of deps) {
|
||||
if (__DEV__) {
|
||||
dep.trigger({
|
||||
target,
|
||||
type,
|
||||
key,
|
||||
newValue,
|
||||
oldValue,
|
||||
oldTarget,
|
||||
})
|
||||
} else {
|
||||
dep.trigger()
|
||||
}
|
||||
}
|
||||
endBatch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test only
|
||||
*/
|
||||
export function getDepFromReactive(object: any, key: string | number | symbol) {
|
||||
return targetMap.get(object)?.get(key)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import { NOOP, extend } from '@vue/shared'
|
||||
import { extend, hasChanged } from '@vue/shared'
|
||||
import type { ComputedRefImpl } from './computed'
|
||||
import {
|
||||
DirtyLevels,
|
||||
type TrackOpTypes,
|
||||
type TriggerOpTypes,
|
||||
} from './constants'
|
||||
import type { Dep } from './dep'
|
||||
import { type EffectScope, recordEffectScope } from './effectScope'
|
||||
import type { TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import { type Dep, globalVersion } from './dep'
|
||||
import { activeEffectScope } from './effectScope'
|
||||
import { warn } from './warning'
|
||||
|
||||
export type EffectScheduler = (...args: any[]) => any
|
||||
|
||||
export type DebuggerEvent = {
|
||||
effect: ReactiveEffect
|
||||
effect: Subscriber
|
||||
} & DebuggerEventExtraInfo
|
||||
|
||||
export type DebuggerEventExtraInfo = {
|
||||
|
|
@ -23,154 +20,13 @@ export type DebuggerEventExtraInfo = {
|
|||
oldTarget?: Map<any, any> | Set<any>
|
||||
}
|
||||
|
||||
export let activeEffect: ReactiveEffect | undefined
|
||||
|
||||
export class ReactiveEffect<T = any> {
|
||||
active = true
|
||||
deps: Dep[] = []
|
||||
|
||||
/**
|
||||
* Can be attached after creation
|
||||
* @internal
|
||||
*/
|
||||
computed?: ComputedRefImpl<T>
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
allowRecurse?: boolean
|
||||
|
||||
onStop?: () => void
|
||||
// dev only
|
||||
onTrack?: (event: DebuggerEvent) => void
|
||||
// dev only
|
||||
onTrigger?: (event: DebuggerEvent) => void
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_dirtyLevel = DirtyLevels.Dirty
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_trackId = 0
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_runnings = 0
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_shouldSchedule = false
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_depsLength = 0
|
||||
|
||||
constructor(
|
||||
public fn: () => T,
|
||||
public trigger: () => void,
|
||||
public scheduler?: EffectScheduler,
|
||||
scope?: EffectScope,
|
||||
) {
|
||||
recordEffectScope(this, scope)
|
||||
}
|
||||
|
||||
public get dirty() {
|
||||
if (
|
||||
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
|
||||
this._dirtyLevel === DirtyLevels.MaybeDirty
|
||||
) {
|
||||
this._dirtyLevel = DirtyLevels.QueryingDirty
|
||||
pauseTracking()
|
||||
for (let i = 0; i < this._depsLength; i++) {
|
||||
const dep = this.deps[i]
|
||||
if (dep.computed) {
|
||||
triggerComputed(dep.computed)
|
||||
if (this._dirtyLevel >= DirtyLevels.Dirty) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
|
||||
this._dirtyLevel = DirtyLevels.NotDirty
|
||||
}
|
||||
resetTracking()
|
||||
}
|
||||
return this._dirtyLevel >= DirtyLevels.Dirty
|
||||
}
|
||||
|
||||
public set dirty(v) {
|
||||
this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
|
||||
}
|
||||
|
||||
run() {
|
||||
this._dirtyLevel = DirtyLevels.NotDirty
|
||||
if (!this.active) {
|
||||
return this.fn()
|
||||
}
|
||||
let lastShouldTrack = shouldTrack
|
||||
let lastEffect = activeEffect
|
||||
try {
|
||||
shouldTrack = true
|
||||
activeEffect = this
|
||||
this._runnings++
|
||||
preCleanupEffect(this)
|
||||
return this.fn()
|
||||
} finally {
|
||||
postCleanupEffect(this)
|
||||
this._runnings--
|
||||
activeEffect = lastEffect
|
||||
shouldTrack = lastShouldTrack
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.active) {
|
||||
preCleanupEffect(this)
|
||||
postCleanupEffect(this)
|
||||
this.onStop?.()
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function triggerComputed(computed: ComputedRefImpl<any>) {
|
||||
return computed.value
|
||||
}
|
||||
|
||||
function preCleanupEffect(effect: ReactiveEffect) {
|
||||
effect._trackId++
|
||||
effect._depsLength = 0
|
||||
}
|
||||
|
||||
function postCleanupEffect(effect: ReactiveEffect) {
|
||||
if (effect.deps.length > effect._depsLength) {
|
||||
for (let i = effect._depsLength; i < effect.deps.length; i++) {
|
||||
cleanupDepEffect(effect.deps[i], effect)
|
||||
}
|
||||
effect.deps.length = effect._depsLength
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
|
||||
const trackId = dep.get(effect)
|
||||
if (trackId !== undefined && effect._trackId !== trackId) {
|
||||
dep.delete(effect)
|
||||
if (dep.size === 0) {
|
||||
dep.cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DebuggerOptions {
|
||||
onTrack?: (event: DebuggerEvent) => void
|
||||
onTrigger?: (event: DebuggerEvent) => void
|
||||
}
|
||||
|
||||
export interface ReactiveEffectOptions extends DebuggerOptions {
|
||||
lazy?: boolean
|
||||
scheduler?: EffectScheduler
|
||||
scope?: EffectScope
|
||||
allowRecurse?: boolean
|
||||
onStop?: () => void
|
||||
}
|
||||
|
|
@ -180,38 +36,416 @@ export interface ReactiveEffectRunner<T = any> {
|
|||
effect: ReactiveEffect
|
||||
}
|
||||
|
||||
export let activeSub: Subscriber | undefined
|
||||
|
||||
export enum EffectFlags {
|
||||
ACTIVE = 1 << 0,
|
||||
RUNNING = 1 << 1,
|
||||
TRACKING = 1 << 2,
|
||||
NOTIFIED = 1 << 3,
|
||||
DIRTY = 1 << 4,
|
||||
ALLOW_RECURSE = 1 << 5,
|
||||
NO_BATCH = 1 << 6,
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given function to track reactive updates.
|
||||
*
|
||||
* The given function will be run once immediately. Every time any reactive
|
||||
* property that's accessed within it gets updated, the function will run again.
|
||||
*
|
||||
* @param fn - The function that will track reactive updates.
|
||||
* @param options - Allows to control the effect's behaviour.
|
||||
* @returns A runner that can be used to control the effect after creation.
|
||||
* Subscriber is a type that tracks (or subscribes to) a list of deps.
|
||||
*/
|
||||
export interface Subscriber extends DebuggerOptions {
|
||||
/**
|
||||
* Head of the doubly linked list representing the deps
|
||||
* @internal
|
||||
*/
|
||||
deps?: Link
|
||||
/**
|
||||
* Tail of the same list
|
||||
* @internal
|
||||
*/
|
||||
depsTail?: Link
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
flags: EffectFlags
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
notify(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a link between a source (Dep) and a subscriber (Effect or Computed).
|
||||
* Deps and subs have a many-to-many relationship - each link between a
|
||||
* dep and a sub is represented by a Link instance.
|
||||
*
|
||||
* A Link is also a node in two doubly-linked lists - one for the associated
|
||||
* sub to track all its deps, and one for the associated dep to track all its
|
||||
* subs.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export interface Link {
|
||||
dep: Dep
|
||||
sub: Subscriber
|
||||
|
||||
/**
|
||||
* - Before each effect run, all previous dep links' version are reset to -1
|
||||
* - During the run, a link's version is synced with the source dep on access
|
||||
* - After the run, links with version -1 (that were never used) are cleaned
|
||||
* up
|
||||
*/
|
||||
version: number
|
||||
|
||||
/**
|
||||
* Pointers for doubly-linked lists
|
||||
*/
|
||||
nextDep?: Link
|
||||
prevDep?: Link
|
||||
|
||||
nextSub?: Link
|
||||
prevSub?: Link
|
||||
|
||||
prevActiveLink?: Link
|
||||
}
|
||||
|
||||
export class ReactiveEffect<T = any>
|
||||
implements Subscriber, ReactiveEffectOptions
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
deps?: Link = undefined
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
depsTail?: Link = undefined
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
nextEffect?: ReactiveEffect = undefined
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
cleanup?: () => void = undefined
|
||||
|
||||
scheduler?: EffectScheduler = undefined
|
||||
onStop?: () => void
|
||||
onTrack?: (event: DebuggerEvent) => void
|
||||
onTrigger?: (event: DebuggerEvent) => void
|
||||
|
||||
constructor(public fn: () => T) {
|
||||
if (activeEffectScope && activeEffectScope.active) {
|
||||
activeEffectScope.effects.push(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
notify() {
|
||||
if (
|
||||
this.flags & EffectFlags.RUNNING &&
|
||||
!(this.flags & EffectFlags.ALLOW_RECURSE)
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (this.flags & EffectFlags.NO_BATCH) {
|
||||
return this.trigger()
|
||||
}
|
||||
if (!(this.flags & EffectFlags.NOTIFIED)) {
|
||||
this.flags |= EffectFlags.NOTIFIED
|
||||
this.nextEffect = batchedEffect
|
||||
batchedEffect = this
|
||||
}
|
||||
}
|
||||
|
||||
run() {
|
||||
// TODO cleanupEffect
|
||||
|
||||
if (!(this.flags & EffectFlags.ACTIVE)) {
|
||||
// stopped during cleanup
|
||||
return this.fn()
|
||||
}
|
||||
|
||||
this.flags |= EffectFlags.RUNNING
|
||||
cleanupEffect(this)
|
||||
prepareDeps(this)
|
||||
const prevEffect = activeSub
|
||||
const prevShouldTrack = shouldTrack
|
||||
activeSub = this
|
||||
shouldTrack = true
|
||||
|
||||
try {
|
||||
return this.fn()
|
||||
} finally {
|
||||
if (__DEV__ && activeSub !== this) {
|
||||
warn(
|
||||
'Active effect was not restored correctly - ' +
|
||||
'this is likely a Vue internal bug.',
|
||||
)
|
||||
}
|
||||
cleanupDeps(this)
|
||||
activeSub = prevEffect
|
||||
shouldTrack = prevShouldTrack
|
||||
this.flags &= ~EffectFlags.RUNNING
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.flags & EffectFlags.ACTIVE) {
|
||||
for (let link = this.deps; link; link = link.nextDep) {
|
||||
removeSub(link)
|
||||
}
|
||||
this.deps = this.depsTail = undefined
|
||||
cleanupEffect(this)
|
||||
this.onStop && this.onStop()
|
||||
this.flags &= ~EffectFlags.ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
trigger() {
|
||||
if (this.scheduler) {
|
||||
this.scheduler()
|
||||
} else {
|
||||
this.runIfDirty()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
runIfDirty() {
|
||||
if (isDirty(this)) {
|
||||
this.run()
|
||||
}
|
||||
}
|
||||
|
||||
get dirty() {
|
||||
return isDirty(this)
|
||||
}
|
||||
}
|
||||
|
||||
let batchDepth = 0
|
||||
let batchedEffect: ReactiveEffect | undefined
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function startBatch() {
|
||||
batchDepth++
|
||||
}
|
||||
|
||||
/**
|
||||
* Run batched effects when all batches have ended
|
||||
* @internal
|
||||
*/
|
||||
export function endBatch() {
|
||||
if (batchDepth > 1) {
|
||||
batchDepth--
|
||||
return
|
||||
}
|
||||
|
||||
let error: unknown
|
||||
while (batchedEffect) {
|
||||
let e: ReactiveEffect | undefined = batchedEffect
|
||||
batchedEffect = undefined
|
||||
while (e) {
|
||||
const next: ReactiveEffect | undefined = e.nextEffect
|
||||
e.nextEffect = undefined
|
||||
e.flags &= ~EffectFlags.NOTIFIED
|
||||
if (e.flags & EffectFlags.ACTIVE) {
|
||||
try {
|
||||
e.trigger()
|
||||
} catch (err) {
|
||||
if (!error) error = err
|
||||
}
|
||||
}
|
||||
e = next
|
||||
}
|
||||
}
|
||||
|
||||
batchDepth--
|
||||
if (error) throw error
|
||||
}
|
||||
|
||||
function prepareDeps(sub: Subscriber) {
|
||||
// Prepare deps for tracking, starting from the head
|
||||
for (let link = sub.deps; link; link = link.nextDep) {
|
||||
// set all previous deps' (if any) version to -1 so that we can track
|
||||
// which ones are unused after the run
|
||||
link.version = -1
|
||||
// store previous active sub if link was being used in another context
|
||||
link.prevActiveLink = link.dep.activeLink
|
||||
link.dep.activeLink = link
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupDeps(sub: Subscriber) {
|
||||
// Cleanup unsued deps
|
||||
let head
|
||||
let tail = sub.depsTail
|
||||
for (let link = tail; link; link = link.prevDep) {
|
||||
if (link.version === -1) {
|
||||
if (link === tail) tail = link.prevDep
|
||||
// unused - remove it from the dep's subscribing effect list
|
||||
removeSub(link)
|
||||
// also remove it from this effect's dep list
|
||||
removeDep(link)
|
||||
} else {
|
||||
// The new head is the last node seen which wasn't removed
|
||||
// from the doubly-linked list
|
||||
head = link
|
||||
}
|
||||
|
||||
// restore previous active link if any
|
||||
link.dep.activeLink = link.prevActiveLink
|
||||
link.prevActiveLink = undefined
|
||||
}
|
||||
// set the new head & tail
|
||||
sub.deps = head
|
||||
sub.depsTail = tail
|
||||
}
|
||||
|
||||
function isDirty(sub: Subscriber): boolean {
|
||||
for (let link = sub.deps; link; link = link.nextDep) {
|
||||
if (
|
||||
link.dep.version !== link.version ||
|
||||
(link.dep.computed && refreshComputed(link.dep.computed) === false) ||
|
||||
link.dep.version !== link.version
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// @ts-expect-error only for backwards compatibility where libs manually set
|
||||
// this flag - e.g. Pinia's testing module
|
||||
if (sub._dirty) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returning false indicates the refresh failed
|
||||
* @internal
|
||||
*/
|
||||
export function refreshComputed(computed: ComputedRefImpl) {
|
||||
if (computed.flags & EffectFlags.RUNNING) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
computed.flags & EffectFlags.TRACKING &&
|
||||
!(computed.flags & EffectFlags.DIRTY)
|
||||
) {
|
||||
return
|
||||
}
|
||||
computed.flags &= ~EffectFlags.DIRTY
|
||||
|
||||
// Global version fast path when no reactive changes has happened since
|
||||
// last refresh.
|
||||
if (computed.globalVersion === globalVersion) {
|
||||
return
|
||||
}
|
||||
computed.globalVersion = globalVersion
|
||||
|
||||
const dep = computed.dep
|
||||
computed.flags |= EffectFlags.RUNNING
|
||||
// In SSR there will be no render effect, so the computed has no subscriber
|
||||
// and therefore tracks no deps, thus we cannot rely on the dirty check.
|
||||
// Instead, computed always re-evaluate and relies on the globalVersion
|
||||
// fast path above for caching.
|
||||
if (dep.version > 0 && !computed.isSSR && !isDirty(computed)) {
|
||||
computed.flags &= ~EffectFlags.RUNNING
|
||||
return
|
||||
}
|
||||
|
||||
const prevSub = activeSub
|
||||
const prevShouldTrack = shouldTrack
|
||||
activeSub = computed
|
||||
shouldTrack = true
|
||||
|
||||
try {
|
||||
prepareDeps(computed)
|
||||
const value = computed.fn()
|
||||
if (dep.version === 0 || hasChanged(value, computed._value)) {
|
||||
computed._value = value
|
||||
dep.version++
|
||||
}
|
||||
} catch (err) {
|
||||
dep.version++
|
||||
}
|
||||
|
||||
activeSub = prevSub
|
||||
shouldTrack = prevShouldTrack
|
||||
cleanupDeps(computed)
|
||||
computed.flags &= ~EffectFlags.RUNNING
|
||||
}
|
||||
|
||||
function removeSub(link: Link) {
|
||||
const { dep, prevSub, nextSub } = link
|
||||
if (prevSub) {
|
||||
prevSub.nextSub = nextSub
|
||||
link.prevSub = undefined
|
||||
}
|
||||
if (nextSub) {
|
||||
nextSub.prevSub = prevSub
|
||||
link.nextSub = undefined
|
||||
}
|
||||
if (dep.subs === link) {
|
||||
// was previous tail, point new tail to prev
|
||||
dep.subs = prevSub
|
||||
}
|
||||
|
||||
if (!dep.subs && dep.computed) {
|
||||
// last subscriber removed
|
||||
// if computed, unsubscribe it from all its deps so this computed and its
|
||||
// value can be GCed
|
||||
dep.computed.flags &= ~EffectFlags.TRACKING
|
||||
for (let l = dep.computed.deps; l; l = l.nextDep) {
|
||||
removeSub(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeDep(link: Link) {
|
||||
const { prevDep, nextDep } = link
|
||||
if (prevDep) {
|
||||
prevDep.nextDep = nextDep
|
||||
link.prevDep = undefined
|
||||
}
|
||||
if (nextDep) {
|
||||
nextDep.prevDep = prevDep
|
||||
link.nextDep = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReactiveEffectRunner<T = any> {
|
||||
(): T
|
||||
effect: ReactiveEffect
|
||||
}
|
||||
|
||||
export function effect<T = any>(
|
||||
fn: () => T,
|
||||
options?: ReactiveEffectOptions,
|
||||
): ReactiveEffectRunner {
|
||||
): ReactiveEffectRunner<T> {
|
||||
if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
|
||||
fn = (fn as ReactiveEffectRunner).effect.fn
|
||||
}
|
||||
|
||||
const _effect = new ReactiveEffect(fn, NOOP, () => {
|
||||
if (_effect.dirty) {
|
||||
_effect.run()
|
||||
}
|
||||
})
|
||||
const e = new ReactiveEffect(fn)
|
||||
if (options) {
|
||||
extend(_effect, options)
|
||||
if (options.scope) recordEffectScope(_effect, options.scope)
|
||||
extend(e, options)
|
||||
}
|
||||
if (!options || !options.lazy) {
|
||||
_effect.run()
|
||||
try {
|
||||
e.run()
|
||||
} catch (err) {
|
||||
e.stop()
|
||||
throw err
|
||||
}
|
||||
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
|
||||
runner.effect = _effect
|
||||
const runner = e.run.bind(e) as ReactiveEffectRunner
|
||||
runner.effect = e
|
||||
return runner
|
||||
}
|
||||
|
||||
|
|
@ -224,9 +458,10 @@ export function stop(runner: ReactiveEffectRunner) {
|
|||
runner.effect.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export let shouldTrack = true
|
||||
export let pauseScheduleStack = 0
|
||||
|
||||
const trackStack: boolean[] = []
|
||||
|
||||
/**
|
||||
|
|
@ -253,75 +488,40 @@ export function resetTracking() {
|
|||
shouldTrack = last === undefined ? true : last
|
||||
}
|
||||
|
||||
export function pauseScheduling() {
|
||||
pauseScheduleStack++
|
||||
}
|
||||
|
||||
export function resetScheduling() {
|
||||
pauseScheduleStack--
|
||||
while (!pauseScheduleStack && queueEffectSchedulers.length) {
|
||||
queueEffectSchedulers.shift()!()
|
||||
/**
|
||||
* Registers a cleanup function for the current active effect.
|
||||
* The cleanup function is called right before the next effect run, or when the
|
||||
* effect is stopped.
|
||||
*
|
||||
* Throws a warning iff there is no currenct active effect. The warning can be
|
||||
* suppressed by passing `true` to the second argument.
|
||||
*
|
||||
* @param fn - the cleanup function to be registered
|
||||
* @param failSilently - if `true`, will not throw warning when called without
|
||||
* an active effect.
|
||||
*/
|
||||
export function onEffectCleanup(fn: () => void, failSilently = false) {
|
||||
if (activeSub instanceof ReactiveEffect) {
|
||||
activeSub.cleanup = fn
|
||||
} else if (__DEV__ && !failSilently) {
|
||||
warn(
|
||||
`onEffectCleanup() was called when there was no active effect` +
|
||||
` to associate with.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function trackEffect(
|
||||
effect: ReactiveEffect,
|
||||
dep: Dep,
|
||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
|
||||
) {
|
||||
if (dep.get(effect) !== effect._trackId) {
|
||||
dep.set(effect, effect._trackId)
|
||||
const oldDep = effect.deps[effect._depsLength]
|
||||
if (oldDep !== dep) {
|
||||
if (oldDep) {
|
||||
cleanupDepEffect(oldDep, effect)
|
||||
}
|
||||
effect.deps[effect._depsLength++] = dep
|
||||
} else {
|
||||
effect._depsLength++
|
||||
}
|
||||
if (__DEV__) {
|
||||
effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
|
||||
function cleanupEffect(e: ReactiveEffect) {
|
||||
const { cleanup } = e
|
||||
e.cleanup = undefined
|
||||
if (cleanup) {
|
||||
// run cleanup without active effect
|
||||
const prevSub = activeSub
|
||||
activeSub = undefined
|
||||
try {
|
||||
cleanup()
|
||||
} finally {
|
||||
activeSub = prevSub
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queueEffectSchedulers: EffectScheduler[] = []
|
||||
|
||||
export function triggerEffects(
|
||||
dep: Dep,
|
||||
dirtyLevel: DirtyLevels,
|
||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
|
||||
) {
|
||||
pauseScheduling()
|
||||
for (const effect of dep.keys()) {
|
||||
// dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
|
||||
let tracking: boolean | undefined
|
||||
if (
|
||||
effect._dirtyLevel < dirtyLevel &&
|
||||
(tracking ??= dep.get(effect) === effect._trackId)
|
||||
) {
|
||||
effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
|
||||
effect._dirtyLevel = dirtyLevel
|
||||
}
|
||||
if (
|
||||
effect._shouldSchedule &&
|
||||
(tracking ??= dep.get(effect) === effect._trackId)
|
||||
) {
|
||||
if (__DEV__) {
|
||||
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
|
||||
}
|
||||
effect.trigger()
|
||||
if (
|
||||
(!effect._runnings || effect.allowRecurse) &&
|
||||
effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
|
||||
) {
|
||||
effect._shouldSchedule = false
|
||||
if (effect.scheduler) {
|
||||
queueEffectSchedulers.push(effect.scheduler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resetScheduling()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ReactiveEffect } from './effect'
|
||||
import { warn } from './warning'
|
||||
|
||||
let activeEffectScope: EffectScope | undefined
|
||||
export let activeEffectScope: EffectScope | undefined
|
||||
|
||||
export class EffectScope {
|
||||
/**
|
||||
|
|
@ -122,15 +122,6 @@ export function effectScope(detached?: boolean) {
|
|||
return new EffectScope(detached)
|
||||
}
|
||||
|
||||
export function recordEffectScope(
|
||||
effect: ReactiveEffect,
|
||||
scope: EffectScope | undefined = activeEffectScope,
|
||||
) {
|
||||
if (scope && scope.active) {
|
||||
scope.effects.push(effect)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current active effect scope if there is one.
|
||||
*
|
||||
|
|
@ -147,10 +138,10 @@ export function getCurrentScope() {
|
|||
* @param fn - The callback function to attach to the scope's cleanup.
|
||||
* @see {@link https://vuejs.org/api/reactivity-advanced.html#onscopedispose}
|
||||
*/
|
||||
export function onScopeDispose(fn: () => void) {
|
||||
export function onScopeDispose(fn: () => void, failSilently = false) {
|
||||
if (activeEffectScope) {
|
||||
activeEffectScope.cleanups.push(fn)
|
||||
} else if (__DEV__) {
|
||||
} else if (__DEV__ && !failSilently) {
|
||||
warn(
|
||||
`onScopeDispose() is called when there is no active effect scope` +
|
||||
` to be associated with.`,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export {
|
|||
shallowReadonly,
|
||||
markRaw,
|
||||
toRaw,
|
||||
toReactive,
|
||||
toReadonly,
|
||||
type Raw,
|
||||
type DeepReadonly,
|
||||
type ShallowReactive,
|
||||
|
|
@ -45,16 +47,15 @@ export {
|
|||
type ComputedSetter,
|
||||
type ComputedRefImpl,
|
||||
} from './computed'
|
||||
export { deferredComputed } from './deferredComputed'
|
||||
export {
|
||||
effect,
|
||||
stop,
|
||||
enableTracking,
|
||||
pauseTracking,
|
||||
resetTracking,
|
||||
pauseScheduling,
|
||||
resetScheduling,
|
||||
onEffectCleanup,
|
||||
ReactiveEffect,
|
||||
EffectFlags,
|
||||
type ReactiveEffectRunner,
|
||||
type ReactiveEffectOptions,
|
||||
type EffectScheduler,
|
||||
|
|
@ -62,13 +63,20 @@ export {
|
|||
type DebuggerEvent,
|
||||
type DebuggerEventExtraInfo,
|
||||
} from './effect'
|
||||
export { trigger, track, ITERATE_KEY } from './reactiveEffect'
|
||||
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'
|
||||
export {
|
||||
baseWatch,
|
||||
|
|
|
|||
|
|
@ -1,150 +0,0 @@
|
|||
import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
|
||||
import { DirtyLevels, type TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import { type Dep, createDep } from './dep'
|
||||
import {
|
||||
activeEffect,
|
||||
pauseScheduling,
|
||||
resetScheduling,
|
||||
shouldTrack,
|
||||
trackEffect,
|
||||
triggerEffects,
|
||||
} from './effect'
|
||||
|
||||
// The main WeakMap that stores {target -> key -> dep} connections.
|
||||
// Conceptually, it's easier to think of a dependency as a Dep class
|
||||
// which maintains a Set of subscribers, but we simply store them as
|
||||
// raw Maps to reduce memory overhead.
|
||||
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 key iterate' : '')
|
||||
|
||||
/**
|
||||
* Tracks access to a reactive property.
|
||||
*
|
||||
* This will check which effect is running at the moment and record it as dep
|
||||
* which records all effects that depend on the reactive property.
|
||||
*
|
||||
* @param target - Object holding the reactive property.
|
||||
* @param type - Defines the type of access to the reactive property.
|
||||
* @param key - Identifier of the reactive property to track.
|
||||
*/
|
||||
export function track(target: object, type: TrackOpTypes, key: unknown) {
|
||||
if (shouldTrack && activeEffect) {
|
||||
let depsMap = targetMap.get(target)
|
||||
if (!depsMap) {
|
||||
targetMap.set(target, (depsMap = new Map()))
|
||||
}
|
||||
let dep = depsMap.get(key)
|
||||
if (!dep) {
|
||||
depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
|
||||
}
|
||||
trackEffect(
|
||||
activeEffect,
|
||||
dep,
|
||||
__DEV__
|
||||
? {
|
||||
target,
|
||||
type,
|
||||
key,
|
||||
}
|
||||
: void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all deps associated with the target (or a specific property) and
|
||||
* triggers the effects stored within.
|
||||
*
|
||||
* @param target - The reactive object.
|
||||
* @param type - Defines the type of the operation that needs to trigger effects.
|
||||
* @param key - Can be used to target a specific reactive property in the target object.
|
||||
*/
|
||||
export function trigger(
|
||||
target: object,
|
||||
type: TriggerOpTypes,
|
||||
key?: unknown,
|
||||
newValue?: unknown,
|
||||
oldValue?: unknown,
|
||||
oldTarget?: Map<unknown, unknown> | Set<unknown>,
|
||||
) {
|
||||
const depsMap = targetMap.get(target)
|
||||
if (!depsMap) {
|
||||
// never been tracked
|
||||
return
|
||||
}
|
||||
|
||||
let deps: (Dep | undefined)[] = []
|
||||
if (type === TriggerOpTypes.CLEAR) {
|
||||
// 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 {
|
||||
// schedule runs for SET | ADD | DELETE
|
||||
if (key !== void 0) {
|
||||
deps.push(depsMap.get(key))
|
||||
}
|
||||
|
||||
// also run for iteration key on ADD | DELETE | Map.SET
|
||||
switch (type) {
|
||||
case TriggerOpTypes.ADD:
|
||||
if (!isArray(target)) {
|
||||
deps.push(depsMap.get(ITERATE_KEY))
|
||||
if (isMap(target)) {
|
||||
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
|
||||
}
|
||||
} else if (isIntegerKey(key)) {
|
||||
// new index added to array -> length changes
|
||||
deps.push(depsMap.get('length'))
|
||||
}
|
||||
break
|
||||
case TriggerOpTypes.DELETE:
|
||||
if (!isArray(target)) {
|
||||
deps.push(depsMap.get(ITERATE_KEY))
|
||||
if (isMap(target)) {
|
||||
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
|
||||
}
|
||||
}
|
||||
break
|
||||
case TriggerOpTypes.SET:
|
||||
if (isMap(target)) {
|
||||
deps.push(depsMap.get(ITERATE_KEY))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pauseScheduling()
|
||||
for (const dep of deps) {
|
||||
if (dep) {
|
||||
triggerEffects(
|
||||
dep,
|
||||
DirtyLevels.Dirty,
|
||||
__DEV__
|
||||
? {
|
||||
target,
|
||||
type,
|
||||
key,
|
||||
newValue,
|
||||
oldValue,
|
||||
oldTarget,
|
||||
}
|
||||
: void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
resetScheduling()
|
||||
}
|
||||
|
||||
export function getDepFromReactive(object: any, key: string | number | symbol) {
|
||||
return targetMap.get(object)?.get(key)
|
||||
}
|
||||
|
|
@ -1,11 +1,3 @@
|
|||
import type { ComputedRef } from './computed'
|
||||
import {
|
||||
activeEffect,
|
||||
shouldTrack,
|
||||
trackEffect,
|
||||
triggerEffects,
|
||||
} from './effect'
|
||||
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import {
|
||||
type IfAny,
|
||||
hasChanged,
|
||||
|
|
@ -13,7 +5,9 @@ import {
|
|||
isFunction,
|
||||
isObject,
|
||||
} from '@vue/shared'
|
||||
import { Dep, getDepFromReactive } from './dep'
|
||||
import {
|
||||
type ShallowReactiveMarker,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
|
|
@ -21,10 +15,8 @@ import {
|
|||
toRaw,
|
||||
toReactive,
|
||||
} from './reactive'
|
||||
import type { ShallowReactiveMarker } from './reactive'
|
||||
import { type Dep, createDep } from './dep'
|
||||
import { ComputedRefImpl } from './computed'
|
||||
import { getDepFromReactive } from './reactiveEffect'
|
||||
import type { ComputedRef } from './computed'
|
||||
import { TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import { warn } from './warning'
|
||||
|
||||
declare const RefSymbol: unique symbol
|
||||
|
|
@ -40,54 +32,6 @@ export interface Ref<T = any> {
|
|||
[RefSymbol]: true
|
||||
}
|
||||
|
||||
type RefBase<T> = {
|
||||
dep?: Dep
|
||||
value: T
|
||||
}
|
||||
|
||||
export function trackRefValue(ref: RefBase<any>) {
|
||||
if (shouldTrack && activeEffect) {
|
||||
ref = toRaw(ref)
|
||||
trackEffect(
|
||||
activeEffect,
|
||||
(ref.dep ??= createDep(
|
||||
() => (ref.dep = undefined),
|
||||
ref instanceof ComputedRefImpl ? ref : undefined,
|
||||
)),
|
||||
__DEV__
|
||||
? {
|
||||
target: ref,
|
||||
type: TrackOpTypes.GET,
|
||||
key: 'value',
|
||||
}
|
||||
: void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerRefValue(
|
||||
ref: RefBase<any>,
|
||||
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
|
||||
newVal?: any,
|
||||
) {
|
||||
ref = toRaw(ref)
|
||||
const dep = ref.dep
|
||||
if (dep) {
|
||||
triggerEffects(
|
||||
dep,
|
||||
dirtyLevel,
|
||||
__DEV__
|
||||
? {
|
||||
target: ref,
|
||||
type: TriggerOpTypes.SET,
|
||||
key: 'value',
|
||||
newValue: newVal,
|
||||
}
|
||||
: void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is a ref object.
|
||||
*
|
||||
|
|
@ -96,7 +40,7 @@ export function triggerRefValue(
|
|||
*/
|
||||
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
|
||||
export function isRef(r: any): r is Ref {
|
||||
return !!(r && r.__v_isRef === true)
|
||||
return r ? r.__v_isRef === true : false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -152,11 +96,15 @@ function createRef(rawValue: unknown, shallow: boolean) {
|
|||
return new RefImpl(rawValue, shallow)
|
||||
}
|
||||
|
||||
class RefImpl<T> {
|
||||
private _value: T
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RefImpl<T = any> {
|
||||
_value: T
|
||||
private _rawValue: T
|
||||
|
||||
public dep?: Dep = undefined
|
||||
dep: Dep = new Dep()
|
||||
|
||||
public readonly __v_isRef = true
|
||||
|
||||
constructor(
|
||||
|
|
@ -168,18 +116,37 @@ class RefImpl<T> {
|
|||
}
|
||||
|
||||
get value() {
|
||||
trackRefValue(this)
|
||||
if (__DEV__) {
|
||||
this.dep.track({
|
||||
target: this,
|
||||
type: TrackOpTypes.GET,
|
||||
key: 'value',
|
||||
})
|
||||
} else {
|
||||
this.dep.track()
|
||||
}
|
||||
return this._value
|
||||
}
|
||||
|
||||
set value(newVal) {
|
||||
set value(newValue) {
|
||||
const oldValue = this._rawValue
|
||||
const useDirectValue =
|
||||
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
|
||||
newVal = useDirectValue ? newVal : toRaw(newVal)
|
||||
if (hasChanged(newVal, this._rawValue)) {
|
||||
this._rawValue = newVal
|
||||
this._value = useDirectValue ? newVal : toReactive(newVal)
|
||||
triggerRefValue(this, DirtyLevels.Dirty, newVal)
|
||||
this.__v_isShallow || isShallow(newValue) || isReadonly(newValue)
|
||||
newValue = useDirectValue ? newValue : toRaw(newValue)
|
||||
if (hasChanged(newValue, oldValue)) {
|
||||
this._rawValue = newValue
|
||||
this._value = useDirectValue ? newValue : toReactive(newValue)
|
||||
if (__DEV__) {
|
||||
this.dep.trigger({
|
||||
target: this,
|
||||
type: TriggerOpTypes.SET,
|
||||
key: 'value',
|
||||
newValue,
|
||||
oldValue,
|
||||
})
|
||||
} else {
|
||||
this.dep.trigger()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -210,7 +177,16 @@ class RefImpl<T> {
|
|||
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
|
||||
*/
|
||||
export function triggerRef(ref: Ref) {
|
||||
triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0)
|
||||
if (__DEV__) {
|
||||
;(ref as unknown as RefImpl).dep.trigger({
|
||||
target: ref,
|
||||
type: TriggerOpTypes.SET,
|
||||
key: 'value',
|
||||
newValue: (ref as unknown as RefImpl)._value,
|
||||
})
|
||||
} else {
|
||||
;(ref as unknown as RefImpl).dep.trigger()
|
||||
}
|
||||
}
|
||||
|
||||
export type MaybeRef<T = any> = T | Ref<T>
|
||||
|
|
@ -296,7 +272,7 @@ export type CustomRefFactory<T> = (
|
|||
}
|
||||
|
||||
class CustomRefImpl<T> {
|
||||
public dep?: Dep = undefined
|
||||
public dep: Dep
|
||||
|
||||
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
|
||||
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
|
||||
|
|
@ -304,10 +280,8 @@ class CustomRefImpl<T> {
|
|||
public readonly __v_isRef = true
|
||||
|
||||
constructor(factory: CustomRefFactory<T>) {
|
||||
const { get, set } = factory(
|
||||
() => trackRefValue(this),
|
||||
() => triggerRefValue(this),
|
||||
)
|
||||
const dep = (this.dep = new Dep())
|
||||
const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
|
||||
this._get = get
|
||||
this._set = set
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
type ComponentInternalInstance,
|
||||
type ComputedRef,
|
||||
type SetupContext,
|
||||
Suspense,
|
||||
computed,
|
||||
|
|
@ -26,6 +25,8 @@ import {
|
|||
withAsyncContext,
|
||||
withDefaults,
|
||||
} from '../src/apiSetupHelpers'
|
||||
import type { ComputedRefImpl } from '../../reactivity/src/computed'
|
||||
import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
|
||||
|
||||
describe('SFC <script setup> helpers', () => {
|
||||
test('should warn runtime usage', () => {
|
||||
|
|
@ -426,7 +427,8 @@ describe('SFC <script setup> helpers', () => {
|
|||
resolve = r
|
||||
})
|
||||
|
||||
let c: ComputedRef
|
||||
let c: ComputedRefImpl
|
||||
let e: ReactiveEffectRunner
|
||||
|
||||
const Comp = defineComponent({
|
||||
async setup() {
|
||||
|
|
@ -435,10 +437,11 @@ describe('SFC <script setup> helpers', () => {
|
|||
__temp = await __temp
|
||||
__restore()
|
||||
|
||||
c = computed(() => {})
|
||||
c = computed(() => {}) as unknown as ComputedRefImpl
|
||||
e = effect(() => c.value)
|
||||
// register the lifecycle after an await statement
|
||||
onMounted(resolve)
|
||||
return () => ''
|
||||
return () => c.value
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -447,10 +450,12 @@ describe('SFC <script setup> helpers', () => {
|
|||
app.mount(root)
|
||||
|
||||
await ready
|
||||
expect(c!.effect.active).toBe(true)
|
||||
expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
|
||||
expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
|
||||
|
||||
app.unmount()
|
||||
expect(c!.effect.active).toBe(false)
|
||||
expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
|
||||
expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from '@vue/runtime-test'
|
||||
import {
|
||||
type DebuggerEvent,
|
||||
EffectFlags,
|
||||
ITERATE_KEY,
|
||||
type Ref,
|
||||
type ShallowRef,
|
||||
|
|
@ -1215,7 +1216,7 @@ describe('api: watch', () => {
|
|||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(instance!.scope.effects[0].active).toBe(false)
|
||||
expect(instance!.scope.effects[0].flags & EffectFlags.ACTIVE).toBeFalsy()
|
||||
})
|
||||
|
||||
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
|
||||
|
|
@ -1504,4 +1505,46 @@ describe('api: watch', () => {
|
|||
unwatch!()
|
||||
expect(scope.effects.length).toBe(0)
|
||||
})
|
||||
|
||||
// simplified case of VueUse syncRef
|
||||
test('sync watcher should not be batched', () => {
|
||||
const a = ref(0)
|
||||
const b = ref(0)
|
||||
let pauseB = false
|
||||
watch(
|
||||
a,
|
||||
() => {
|
||||
pauseB = true
|
||||
b.value = a.value + 1
|
||||
pauseB = false
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
watch(
|
||||
b,
|
||||
() => {
|
||||
if (!pauseB) {
|
||||
throw new Error('should not be called')
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
|
||||
a.value = 1
|
||||
expect(b.value).toBe(2)
|
||||
})
|
||||
|
||||
test('watchEffect should not fire on computed deps that did not change', async () => {
|
||||
const a = ref(0)
|
||||
const c = computed(() => a.value % 2)
|
||||
const spy = vi.fn()
|
||||
watchEffect(() => {
|
||||
spy()
|
||||
c.value
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
a.value += 2
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import {
|
||||
type SchedulerJob,
|
||||
SchedulerJobFlags,
|
||||
flushPostFlushCbs,
|
||||
flushPreFlushCbs,
|
||||
invalidateJob,
|
||||
|
|
@ -119,12 +121,12 @@ describe('scheduler', () => {
|
|||
const job1 = () => {
|
||||
calls.push('job1')
|
||||
}
|
||||
const cb1 = () => {
|
||||
const cb1: SchedulerJob = () => {
|
||||
// queueJob in postFlushCb
|
||||
calls.push('cb1')
|
||||
queueJob(job1)
|
||||
}
|
||||
cb1.pre = true
|
||||
cb1.flags! |= SchedulerJobFlags.PRE
|
||||
|
||||
queueJob(cb1)
|
||||
await nextTick()
|
||||
|
|
@ -138,25 +140,25 @@ describe('scheduler', () => {
|
|||
}
|
||||
job1.id = 1
|
||||
|
||||
const cb1 = () => {
|
||||
const cb1: SchedulerJob = () => {
|
||||
calls.push('cb1')
|
||||
queueJob(job1)
|
||||
// cb2 should execute before the job
|
||||
queueJob(cb2)
|
||||
queueJob(cb3)
|
||||
}
|
||||
cb1.pre = true
|
||||
cb1.flags! |= SchedulerJobFlags.PRE
|
||||
|
||||
const cb2 = () => {
|
||||
const cb2: SchedulerJob = () => {
|
||||
calls.push('cb2')
|
||||
}
|
||||
cb2.pre = true
|
||||
cb2.flags! |= SchedulerJobFlags.PRE
|
||||
cb2.id = 1
|
||||
|
||||
const cb3 = () => {
|
||||
const cb3: SchedulerJob = () => {
|
||||
calls.push('cb3')
|
||||
}
|
||||
cb3.pre = true
|
||||
cb3.flags! |= SchedulerJobFlags.PRE
|
||||
cb3.id = 1
|
||||
|
||||
queueJob(cb1)
|
||||
|
|
@ -166,37 +168,37 @@ describe('scheduler', () => {
|
|||
|
||||
it('should insert jobs after pre jobs with the same id', async () => {
|
||||
const calls: string[] = []
|
||||
const job1 = () => {
|
||||
const job1: SchedulerJob = () => {
|
||||
calls.push('job1')
|
||||
}
|
||||
job1.id = 1
|
||||
job1.pre = true
|
||||
const job2 = () => {
|
||||
job1.flags! |= SchedulerJobFlags.PRE
|
||||
const job2: SchedulerJob = () => {
|
||||
calls.push('job2')
|
||||
queueJob(job5)
|
||||
queueJob(job6)
|
||||
}
|
||||
job2.id = 2
|
||||
job2.pre = true
|
||||
const job3 = () => {
|
||||
job2.flags! |= SchedulerJobFlags.PRE
|
||||
const job3: SchedulerJob = () => {
|
||||
calls.push('job3')
|
||||
}
|
||||
job3.id = 2
|
||||
job3.pre = true
|
||||
const job4 = () => {
|
||||
job3.flags! |= SchedulerJobFlags.PRE
|
||||
const job4: SchedulerJob = () => {
|
||||
calls.push('job4')
|
||||
}
|
||||
job4.id = 3
|
||||
job4.pre = true
|
||||
const job5 = () => {
|
||||
job4.flags! |= SchedulerJobFlags.PRE
|
||||
const job5: SchedulerJob = () => {
|
||||
calls.push('job5')
|
||||
}
|
||||
job5.id = 2
|
||||
const job6 = () => {
|
||||
const job6: SchedulerJob = () => {
|
||||
calls.push('job6')
|
||||
}
|
||||
job6.id = 2
|
||||
job6.pre = true
|
||||
job6.flags! |= SchedulerJobFlags.PRE
|
||||
|
||||
// We need several jobs to test this properly, otherwise
|
||||
// findInsertionIndex can yield the correct index by chance
|
||||
|
|
@ -221,16 +223,16 @@ describe('scheduler', () => {
|
|||
flushPreFlushCbs()
|
||||
calls.push('job1')
|
||||
}
|
||||
const cb1 = () => {
|
||||
const cb1: SchedulerJob = () => {
|
||||
calls.push('cb1')
|
||||
// a cb triggers its parent job, which should be skipped
|
||||
queueJob(job1)
|
||||
}
|
||||
cb1.pre = true
|
||||
const cb2 = () => {
|
||||
cb1.flags! |= SchedulerJobFlags.PRE
|
||||
const cb2: SchedulerJob = () => {
|
||||
calls.push('cb2')
|
||||
}
|
||||
cb2.pre = true
|
||||
cb2.flags! |= SchedulerJobFlags.PRE
|
||||
|
||||
queueJob(job1)
|
||||
await nextTick()
|
||||
|
|
@ -240,8 +242,8 @@ describe('scheduler', () => {
|
|||
// #3806
|
||||
it('queue preFlushCb inside postFlushCb', async () => {
|
||||
const spy = vi.fn()
|
||||
const cb = () => spy()
|
||||
cb.pre = true
|
||||
const cb: SchedulerJob = () => spy()
|
||||
cb.flags! |= SchedulerJobFlags.PRE
|
||||
queuePostFlushCb(() => {
|
||||
queueJob(cb)
|
||||
})
|
||||
|
|
@ -521,25 +523,25 @@ describe('scheduler', () => {
|
|||
test('should allow explicitly marked jobs to trigger itself', async () => {
|
||||
// normal job
|
||||
let count = 0
|
||||
const job = () => {
|
||||
const job: SchedulerJob = () => {
|
||||
if (count < 3) {
|
||||
count++
|
||||
queueJob(job)
|
||||
}
|
||||
}
|
||||
job.allowRecurse = true
|
||||
job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
|
||||
queueJob(job)
|
||||
await nextTick()
|
||||
expect(count).toBe(3)
|
||||
|
||||
// post cb
|
||||
const cb = () => {
|
||||
const cb: SchedulerJob = () => {
|
||||
if (count < 5) {
|
||||
count++
|
||||
queuePostFlushCb(cb)
|
||||
}
|
||||
}
|
||||
cb.allowRecurse = true
|
||||
cb.flags! |= SchedulerJobFlags.ALLOW_RECURSE
|
||||
queuePostFlushCb(cb)
|
||||
await nextTick()
|
||||
expect(count).toBe(5)
|
||||
|
|
@ -572,7 +574,7 @@ describe('scheduler', () => {
|
|||
// simulate parent component that toggles child
|
||||
const job1 = () => {
|
||||
// @ts-expect-error
|
||||
job2.active = false
|
||||
job2.flags! |= SchedulerJobFlags.DISPOSED
|
||||
}
|
||||
// simulate child that's triggered by the same reactive change that
|
||||
// triggers its toggle
|
||||
|
|
@ -589,11 +591,11 @@ describe('scheduler', () => {
|
|||
|
||||
it('flushPreFlushCbs inside a pre job', async () => {
|
||||
const spy = vi.fn()
|
||||
const job = () => {
|
||||
const job: SchedulerJob = () => {
|
||||
spy()
|
||||
flushPreFlushCbs()
|
||||
}
|
||||
job.pre = true
|
||||
job.flags! |= SchedulerJobFlags.PRE
|
||||
queueJob(job)
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
|
|
|||
|
|
@ -187,7 +187,6 @@ export function defineAsyncComponent<
|
|||
if (instance.parent && isKeepAlive(instance.parent.vnode)) {
|
||||
// parent is keep-alive, force update so the loaded component's
|
||||
// name is taken into account
|
||||
instance.parent.effect.dirty = true
|
||||
queueJob(instance.parent.update)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -284,9 +284,13 @@ export interface ComponentInternalInstance {
|
|||
*/
|
||||
effect: ReactiveEffect
|
||||
/**
|
||||
* Bound effect runner to be passed to schedulers
|
||||
* Force update render effect
|
||||
*/
|
||||
update: SchedulerJob
|
||||
update: () => void
|
||||
/**
|
||||
* Render effect job to be passed to scheduler (checks if dirty)
|
||||
*/
|
||||
job: SchedulerJob
|
||||
/**
|
||||
* The render function that returns vdom tree.
|
||||
* @internal
|
||||
|
|
@ -550,6 +554,7 @@ export function createComponentInstance(
|
|||
subTree: null!, // will be set synchronously right after creation
|
||||
effect: null!,
|
||||
update: null!, // will be set synchronously right after creation
|
||||
job: null!,
|
||||
scope: new EffectScope(true /* detached */),
|
||||
render: null,
|
||||
proxy: null,
|
||||
|
|
|
|||
|
|
@ -276,7 +276,6 @@ export const publicPropertiesMap: PublicPropertiesMap =
|
|||
$forceUpdate: i =>
|
||||
i.f ||
|
||||
(i.f = () => {
|
||||
i.effect.dirty = true
|
||||
queueJob(i.update)
|
||||
}),
|
||||
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
|
|||
import { PatchFlags, ShapeFlags, isArray } from '@vue/shared'
|
||||
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
|
||||
import type { RendererElement } from '../renderer'
|
||||
import { SchedulerJobFlags } from '../scheduler'
|
||||
|
||||
type Hook<T = () => void> = T | T[]
|
||||
|
||||
|
|
@ -231,8 +232,7 @@ const BaseTransitionImpl: ComponentOptions = {
|
|||
state.isLeaving = false
|
||||
// #6835
|
||||
// it also needs to be updated when active is undefined
|
||||
if (instance.update.active !== false) {
|
||||
instance.effect.dirty = true
|
||||
if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
|
||||
instance.update()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ export function initCustomFormatter() {
|
|||
{},
|
||||
['span', vueStyle, genRefFlag(obj)],
|
||||
'<',
|
||||
formatValue(obj.value),
|
||||
// avoid debugger accessing value affecting behavior
|
||||
formatValue('_value' in obj ? obj._value : obj),
|
||||
`>`,
|
||||
]
|
||||
} else if (isReactive(obj)) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ function rerender(id: string, newRender?: Function) {
|
|||
instance.renderCache = []
|
||||
// this flag forces child components with slot content to update
|
||||
isHmrUpdating = true
|
||||
instance.effect.dirty = true
|
||||
instance.update()
|
||||
isHmrUpdating = false
|
||||
})
|
||||
|
|
@ -138,7 +137,6 @@ function reload(id: string, newComp: HMRComponent) {
|
|||
// 4. Force the parent instance to re-render. This will cause all updated
|
||||
// components to be unmounted and re-mounted. Queue the update so that we
|
||||
// don't end up forcing the same parent to re-render multiple times.
|
||||
instance.parent.effect.dirty = true
|
||||
queueJob(instance.parent.update)
|
||||
} else if (instance.appContext.reload) {
|
||||
// root instance mounted via createApp() has a reload method
|
||||
|
|
|
|||
|
|
@ -40,13 +40,19 @@ import {
|
|||
import {
|
||||
type SchedulerFactory,
|
||||
type SchedulerJob,
|
||||
SchedulerJobFlags,
|
||||
flushPostFlushCbs,
|
||||
flushPreFlushCbs,
|
||||
invalidateJob,
|
||||
queueJob,
|
||||
queuePostFlushCb,
|
||||
} from './scheduler'
|
||||
import { ReactiveEffect, pauseTracking, resetTracking } from '@vue/reactivity'
|
||||
import {
|
||||
EffectFlags,
|
||||
ReactiveEffect,
|
||||
pauseTracking,
|
||||
resetTracking,
|
||||
} from '@vue/reactivity'
|
||||
import { updateProps } from './componentProps'
|
||||
import { updateSlots } from './componentSlots'
|
||||
import { popWarningContext, pushWarningContext, warn } from './warning'
|
||||
|
|
@ -1302,7 +1308,6 @@ function baseCreateRenderer(
|
|||
// double updating the same child component in the same flush.
|
||||
invalidateJob(instance.update)
|
||||
// instance.update is the reactive effect.
|
||||
instance.effect.dirty = true
|
||||
instance.update()
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1587,19 +1592,15 @@ function baseCreateRenderer(
|
|||
}
|
||||
|
||||
// create reactive effect for rendering
|
||||
const effect = (instance.effect = new ReactiveEffect(
|
||||
componentUpdateFn,
|
||||
NOOP,
|
||||
() => queueJob(update),
|
||||
instance.scope, // track it in component's effect scope
|
||||
))
|
||||
instance.scope.on()
|
||||
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
|
||||
instance.scope.off()
|
||||
|
||||
const update = (instance.update = effect.run.bind(effect))
|
||||
const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
|
||||
job.id = instance.uid
|
||||
effect.scheduler = () => queueJob(job)
|
||||
|
||||
const update: SchedulerJob = (instance.update = () => {
|
||||
if (effect.dirty) {
|
||||
effect.run()
|
||||
}
|
||||
})
|
||||
update.id = instance.uid
|
||||
// allowRecurse
|
||||
// #1801, #2043 component render effects should allow recursive updates
|
||||
toggleRecurse(instance, true)
|
||||
|
|
@ -1611,7 +1612,7 @@ function baseCreateRenderer(
|
|||
effect.onTrigger = instance.rtg
|
||||
? e => invokeArrayFns(instance.rtg!, e)
|
||||
: void 0
|
||||
update.ownerInstance = instance
|
||||
job.ownerInstance = instance
|
||||
}
|
||||
|
||||
update()
|
||||
|
|
@ -2278,7 +2279,7 @@ function baseCreateRenderer(
|
|||
unregisterHMR(instance)
|
||||
}
|
||||
|
||||
const { bum, scope, update, subTree, um } = instance
|
||||
const { bum, scope, job, subTree, um } = instance
|
||||
|
||||
// beforeUnmount hook
|
||||
if (bum) {
|
||||
|
|
@ -2295,11 +2296,11 @@ function baseCreateRenderer(
|
|||
// stop effects in component scope
|
||||
scope.stop()
|
||||
|
||||
// update may be null if a component is unmounted before its async
|
||||
// job may be null if a component is unmounted before its async
|
||||
// setup has resolved.
|
||||
if (update) {
|
||||
if (job) {
|
||||
// so that scheduler will no longer invoke it
|
||||
update.active = false
|
||||
job.flags! |= SchedulerJobFlags.DISPOSED
|
||||
unmount(subTree, instance, parentSuspense, doRemove)
|
||||
}
|
||||
// unmounted hook
|
||||
|
|
@ -2434,10 +2435,16 @@ function resolveChildrenNamespace(
|
|||
}
|
||||
|
||||
function toggleRecurse(
|
||||
{ effect, update }: ComponentInternalInstance,
|
||||
{ effect, job }: ComponentInternalInstance,
|
||||
allowed: boolean,
|
||||
) {
|
||||
effect.allowRecurse = update.allowRecurse = allowed
|
||||
if (allowed) {
|
||||
effect.flags |= EffectFlags.ALLOW_RECURSE
|
||||
job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
|
||||
} else {
|
||||
effect.flags &= ~EffectFlags.ALLOW_RECURSE
|
||||
job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE
|
||||
}
|
||||
}
|
||||
|
||||
export function needTransition(
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@ import { type Awaited, NOOP, isArray } from '@vue/shared'
|
|||
import { type ComponentInternalInstance, getComponentName } from './component'
|
||||
import type { Scheduler } from '@vue/reactivity'
|
||||
|
||||
export interface SchedulerJob extends Function {
|
||||
id?: number
|
||||
pre?: boolean
|
||||
active?: boolean
|
||||
computed?: boolean
|
||||
export enum SchedulerJobFlags {
|
||||
QUEUED = 1 << 0,
|
||||
PRE = 1 << 1,
|
||||
/**
|
||||
* Indicates whether the effect is allowed to recursively trigger itself
|
||||
* when managed by the scheduler.
|
||||
|
|
@ -23,7 +21,17 @@ export interface SchedulerJob extends Function {
|
|||
* responsibility to perform recursive state mutation that eventually
|
||||
* stabilizes (#1727).
|
||||
*/
|
||||
allowRecurse?: boolean
|
||||
ALLOW_RECURSE = 1 << 2,
|
||||
DISPOSED = 1 << 3,
|
||||
}
|
||||
|
||||
export interface SchedulerJob extends Function {
|
||||
id?: number
|
||||
/**
|
||||
* flags can technically be undefined, but it can still be used in bitwise
|
||||
* operations just like 0.
|
||||
*/
|
||||
flags?: SchedulerJobFlags
|
||||
/**
|
||||
* Attached by renderer.ts when setting up a component's render effect
|
||||
* Used to obtain component information when reporting max recursive updates.
|
||||
|
|
@ -71,7 +79,10 @@ function findInsertionIndex(id: number) {
|
|||
const middle = (start + end) >>> 1
|
||||
const middleJob = queue[middle]
|
||||
const middleJobId = getId(middleJob)
|
||||
if (middleJobId < id || (middleJobId === id && middleJob.pre)) {
|
||||
if (
|
||||
middleJobId < id ||
|
||||
(middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
|
||||
) {
|
||||
start = middle + 1
|
||||
} else {
|
||||
end = middle
|
||||
|
|
@ -82,24 +93,22 @@ function findInsertionIndex(id: number) {
|
|||
}
|
||||
|
||||
export function queueJob(job: SchedulerJob) {
|
||||
// the dedupe search uses the startIndex argument of Array.includes()
|
||||
// by default the search index includes the current job that is being run
|
||||
// so it cannot recursively trigger itself again.
|
||||
// if the job is a watch() callback, the search will start with a +1 index to
|
||||
// allow it recursively trigger itself - it is the user's responsibility to
|
||||
// ensure it doesn't end up in an infinite loop.
|
||||
if (
|
||||
!queue.length ||
|
||||
!queue.includes(
|
||||
job,
|
||||
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
|
||||
)
|
||||
) {
|
||||
if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
|
||||
if (job.id == null) {
|
||||
queue.push(job)
|
||||
} else if (
|
||||
// fast path when the job id is larger than the tail
|
||||
!(job.flags! & SchedulerJobFlags.PRE) &&
|
||||
job.id >= (queue[queue.length - 1]?.id || 0)
|
||||
) {
|
||||
queue.push(job)
|
||||
} else {
|
||||
queue.splice(findInsertionIndex(job.id), 0, job)
|
||||
}
|
||||
|
||||
if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
|
||||
job.flags! |= SchedulerJobFlags.QUEUED
|
||||
}
|
||||
queueFlush()
|
||||
}
|
||||
}
|
||||
|
|
@ -120,14 +129,11 @@ export function invalidateJob(job: SchedulerJob) {
|
|||
|
||||
export function queuePostFlushCb(cb: SchedulerJobs) {
|
||||
if (!isArray(cb)) {
|
||||
if (
|
||||
!activePostFlushCbs ||
|
||||
!activePostFlushCbs.includes(
|
||||
cb,
|
||||
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex,
|
||||
)
|
||||
) {
|
||||
if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
|
||||
pendingPostFlushCbs.push(cb)
|
||||
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
|
||||
cb.flags! |= SchedulerJobFlags.QUEUED
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if cb is an array, it is a component lifecycle hook which can only be
|
||||
|
|
@ -149,7 +155,7 @@ export function flushPreFlushCbs(
|
|||
}
|
||||
for (; i < queue.length; i++) {
|
||||
const cb = queue[i]
|
||||
if (cb && cb.pre) {
|
||||
if (cb && cb.flags! & SchedulerJobFlags.PRE) {
|
||||
if (instance && cb.id !== instance.uid) {
|
||||
continue
|
||||
}
|
||||
|
|
@ -159,6 +165,7 @@ export function flushPreFlushCbs(
|
|||
queue.splice(i, 1)
|
||||
i--
|
||||
cb()
|
||||
cb.flags! &= ~SchedulerJobFlags.QUEUED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -193,6 +200,7 @@ export function flushPostFlushCbs(seen?: CountMap) {
|
|||
continue
|
||||
}
|
||||
activePostFlushCbs[postFlushIndex]()
|
||||
activePostFlushCbs[postFlushIndex].flags! &= ~SchedulerJobFlags.QUEUED
|
||||
}
|
||||
activePostFlushCbs = null
|
||||
postFlushIndex = 0
|
||||
|
|
@ -205,8 +213,10 @@ const getId = (job: SchedulerJob): number =>
|
|||
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
|
||||
const diff = getId(a) - getId(b)
|
||||
if (diff === 0) {
|
||||
if (a.pre && !b.pre) return -1
|
||||
if (b.pre && !a.pre) return 1
|
||||
const isAPre = a.flags! & SchedulerJobFlags.PRE
|
||||
const isBPre = b.flags! & SchedulerJobFlags.PRE
|
||||
if (isAPre && !isBPre) return -1
|
||||
if (isBPre && !isAPre) return 1
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
|
@ -239,11 +249,12 @@ function flushJobs(seen?: CountMap) {
|
|||
try {
|
||||
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
|
||||
const job = queue[flushIndex]
|
||||
if (job && job.active !== false) {
|
||||
if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
|
||||
if (__DEV__ && check(job)) {
|
||||
continue
|
||||
}
|
||||
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
|
||||
job.flags! &= ~SchedulerJobFlags.QUEUED
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { computed, createSSRApp, defineComponent, h, reactive } from 'vue'
|
||||
import { computed, createSSRApp, defineComponent, h, reactive, ref } from 'vue'
|
||||
import { renderToString } from '../src/renderToString'
|
||||
|
||||
// #5208 reported memory leak of keeping computed alive during SSR
|
||||
|
|
@ -45,3 +45,23 @@ test('computed reactivity during SSR', async () => {
|
|||
// during the render phase
|
||||
expect(getterSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
// although we technically shouldn't allow state mutation during render,
|
||||
// it does sometimes happen
|
||||
test('computed mutation during render', async () => {
|
||||
const App = defineComponent(async () => {
|
||||
const n = ref(0)
|
||||
const m = computed(() => n.value + 1)
|
||||
|
||||
m.value // force non-dirty
|
||||
|
||||
return () => {
|
||||
n.value++
|
||||
return h('div', null, `value: ${m.value}`)
|
||||
}
|
||||
})
|
||||
|
||||
const app = createSSRApp(App)
|
||||
const html = await renderToString(app)
|
||||
expect(html).toMatch('value: 2')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -143,15 +143,6 @@ function renderComponentSubTree(
|
|||
comp.ssrRender = ssrCompile(comp.template, instance)
|
||||
}
|
||||
|
||||
// perf: enable caching of computed getters during render
|
||||
// since there cannot be state mutations during render.
|
||||
for (const e of instance.scope.effects) {
|
||||
if (e.computed) {
|
||||
e.computed._dirty = true
|
||||
e.computed._cacheable = true
|
||||
}
|
||||
}
|
||||
|
||||
const ssrRender = instance.ssrRender || comp.ssrRender
|
||||
if (ssrRender) {
|
||||
// optimized
|
||||
|
|
|
|||
Loading…
Reference in New Issue