From 05eb4e0fefd585125dd60b7f8fe9c36928d921aa Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 25 Feb 2024 16:51:49 +0800 Subject: [PATCH 001/242] Refactor reactivity system to use version counting and doubly-linked list tracking (#10397) Bug fixes close #10236 close #10069 PRs made stale by this one close #10290 close #10354 close #10189 close #9480 --- .../__benchmarks__/computed.bench.ts | 200 +++++++ .../reactivity/__benchmarks__/effect.bench.ts | 111 ++++ .../reactiveArray.bench.ts | 8 +- .../reactiveMap.bench.ts | 4 +- .../__benchmarks__/reactiveObject.bench.ts | 21 + .../ref.bench.ts | 1 - .../reactivity/__tests__/computed.bench.ts | 126 ---- .../reactivity/__tests__/computed.spec.ts | 396 ++++++++++--- .../__tests__/deferredComputed.spec.ts | 155 ----- packages/reactivity/__tests__/effect.spec.ts | 105 ++-- .../reactivity/__tests__/effectScope.spec.ts | 4 +- packages/reactivity/__tests__/gc.spec.ts | 2 +- .../__tests__/reactiveObject.bench.ts | 114 ---- .../reactivity/__tests__/readonly.spec.ts | 2 +- packages/reactivity/__tests__/ref.spec.ts | 11 + .../__tests__/shallowReactive.spec.ts | 1 + packages/reactivity/src/baseHandlers.ts | 22 +- packages/reactivity/src/collectionHandlers.ts | 7 +- packages/reactivity/src/computed.ts | 152 ++--- packages/reactivity/src/deferredComputed.ts | 6 - packages/reactivity/src/dep.ts | 302 +++++++++- packages/reactivity/src/effect.ts | 549 +++++++++++------- packages/reactivity/src/index.ts | 6 +- packages/reactivity/src/reactiveEffect.ts | 150 ----- packages/reactivity/src/ref.ts | 130 ++--- .../__tests__/apiSetupHelpers.spec.ts | 17 +- .../runtime-core/__tests__/apiWatch.spec.ts | 45 +- .../runtime-core/src/apiAsyncComponent.ts | 1 - packages/runtime-core/src/apiWatch.ts | 16 +- packages/runtime-core/src/component.ts | 9 +- .../src/componentPublicInstance.ts | 1 - .../src/components/BaseTransition.ts | 3 +- packages/runtime-core/src/customFormatter.ts | 3 +- packages/runtime-core/src/hmr.ts | 2 - packages/runtime-core/src/renderer.ts | 37 +- packages/runtime-core/src/scheduler.ts | 1 - .../__tests__/ssrComputed.spec.ts | 22 +- packages/server-renderer/src/render.ts | 9 - 38 files changed, 1629 insertions(+), 1122 deletions(-) create mode 100644 packages/reactivity/__benchmarks__/computed.bench.ts create mode 100644 packages/reactivity/__benchmarks__/effect.bench.ts rename packages/reactivity/{__tests__ => __benchmarks__}/reactiveArray.bench.ts (94%) rename packages/reactivity/{__tests__ => __benchmarks__}/reactiveMap.bench.ts (97%) create mode 100644 packages/reactivity/__benchmarks__/reactiveObject.bench.ts rename packages/reactivity/{__tests__ => __benchmarks__}/ref.bench.ts (99%) delete mode 100644 packages/reactivity/__tests__/computed.bench.ts delete mode 100644 packages/reactivity/__tests__/deferredComputed.spec.ts delete mode 100644 packages/reactivity/__tests__/reactiveObject.bench.ts delete mode 100644 packages/reactivity/src/deferredComputed.ts delete mode 100644 packages/reactivity/src/reactiveEffect.ts diff --git a/packages/reactivity/__benchmarks__/computed.bench.ts b/packages/reactivity/__benchmarks__/computed.bench.ts new file mode 100644 index 00000000000..d9757501f81 --- /dev/null +++ b/packages/reactivity/__benchmarks__/computed.bench.ts @@ -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[] = [] + 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[] = [] + 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[] = [] + 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[] = [] + 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[] = [] + 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[] = [] + 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[] = [] + 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[] = [] + 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 + }) + } +}) diff --git a/packages/reactivity/__benchmarks__/effect.bench.ts b/packages/reactivity/__benchmarks__/effect.bench.ts new file mode 100644 index 00000000000..8d3d6ecfbfc --- /dev/null +++ b/packages/reactivity/__benchmarks__/effect.bench.ts @@ -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) +}) diff --git a/packages/reactivity/__tests__/reactiveArray.bench.ts b/packages/reactivity/__benchmarks__/reactiveArray.bench.ts similarity index 94% rename from packages/reactivity/__tests__/reactiveArray.bench.ts rename to packages/reactivity/__benchmarks__/reactiveArray.bench.ts index 9ce0dc531d1..6726cccfd89 100644 --- a/packages/reactivity/__tests__/reactiveArray.bench.ts +++ b/packages/reactivity/__benchmarks__/reactiveArray.bench.ts @@ -3,7 +3,7 @@ import { computed, reactive, readonly, shallowRef, triggerRef } from '../src' for (let amount = 1e1; amount < 1e4; amount *= 10) { { - const rawArray = [] + const rawArray: any[] = [] for (let i = 0, n = amount; i < n; i++) { rawArray.push(i) } @@ -21,7 +21,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { } { - const rawArray = [] + const rawArray: any[] = [] for (let i = 0, n = amount; i < n; i++) { rawArray.push(i) } @@ -40,7 +40,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { } { - const rawArray = [] + const rawArray: any[] = [] for (let i = 0, n = amount; i < n; i++) { rawArray.push(i) } @@ -56,7 +56,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) { } { - const rawArray = [] + const rawArray: any[] = [] for (let i = 0, n = amount; i < n; i++) { rawArray.push(i) } diff --git a/packages/reactivity/__tests__/reactiveMap.bench.ts b/packages/reactivity/__benchmarks__/reactiveMap.bench.ts similarity index 97% rename from packages/reactivity/__tests__/reactiveMap.bench.ts rename to packages/reactivity/__benchmarks__/reactiveMap.bench.ts index 70a034e96c3..f8b4611153e 100644 --- a/packages/reactivity/__tests__/reactiveMap.bench.ts +++ b/packages/reactivity/__benchmarks__/reactiveMap.bench.ts @@ -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 diff --git a/packages/reactivity/__benchmarks__/reactiveObject.bench.ts b/packages/reactivity/__benchmarks__/reactiveObject.bench.ts new file mode 100644 index 00000000000..a326a111b49 --- /dev/null +++ b/packages/reactivity/__benchmarks__/reactiveObject.bench.ts @@ -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++ + }) +} diff --git a/packages/reactivity/__tests__/ref.bench.ts b/packages/reactivity/__benchmarks__/ref.bench.ts similarity index 99% rename from packages/reactivity/__tests__/ref.bench.ts rename to packages/reactivity/__benchmarks__/ref.bench.ts index 286d53e8840..0c05890179b 100644 --- a/packages/reactivity/__tests__/ref.bench.ts +++ b/packages/reactivity/__benchmarks__/ref.bench.ts @@ -26,7 +26,6 @@ describe('ref', () => { const v = ref(100) bench('write/read ref', () => { v.value = i++ - v.value }) } diff --git a/packages/reactivity/__tests__/computed.bench.ts b/packages/reactivity/__tests__/computed.bench.ts deleted file mode 100644 index 0ffa288ff1e..00000000000 --- a/packages/reactivity/__tests__/computed.bench.ts +++ /dev/null @@ -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[] = [] - 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[] = [] - 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 - }) - } -}) diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index c9f47720edd..e2325be54d2 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -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']) }) }) diff --git a/packages/reactivity/__tests__/deferredComputed.spec.ts b/packages/reactivity/__tests__/deferredComputed.spec.ts deleted file mode 100644 index 8e78ba959c3..00000000000 --- a/packages/reactivity/__tests__/deferredComputed.spec.ts +++ /dev/null @@ -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) - }) -}) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index bd26934f1ce..99453d35d87 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -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,12 @@ import { render, serializeInner, } from '@vue/runtime-test' +import { + endBatch, + pauseTracking, + resetTracking, + startBatch, +} from '../src/effect' describe('reactivity/effect', () => { it('should run the passed function once (wrapped by a effect)', () => { @@ -698,18 +703,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 +998,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 +1006,10 @@ describe('reactivity/effect', () => { counterSpy.mockClear() - pauseScheduling() + startBatch() counter.num++ counter.num++ - resetScheduling() + endBatch() expect(counterSpy).toHaveBeenCalledTimes(1) }) @@ -1049,47 +1042,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 +1120,15 @@ 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) }) }) }) diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts index f7e3241ccd6..8a7f26dbb2d 100644 --- a/packages/reactivity/__tests__/effectScope.spec.ts +++ b/packages/reactivity/__tests__/effectScope.spec.ts @@ -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) diff --git a/packages/reactivity/__tests__/gc.spec.ts b/packages/reactivity/__tests__/gc.spec.ts index 953765dd1d9..678600751a9 100644 --- a/packages/reactivity/__tests__/gc.spec.ts +++ b/packages/reactivity/__tests__/gc.spec.ts @@ -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 = () => { diff --git a/packages/reactivity/__tests__/reactiveObject.bench.ts b/packages/reactivity/__tests__/reactiveObject.bench.ts deleted file mode 100644 index 71632283f69..00000000000 --- a/packages/reactivity/__tests__/reactiveObject.bench.ts +++ /dev/null @@ -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[] = [] - 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[] = [] - 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 - }) -} diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index 66da71a8c9e..e86c7fa5b50 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -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)', () => { diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index 2b2024d9723..ed917dbdd92 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -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) + }) }) diff --git a/packages/reactivity/__tests__/shallowReactive.spec.ts b/packages/reactivity/__tests__/shallowReactive.spec.ts index e9b64d39b36..a5218658a27 100644 --- a/packages/reactivity/__tests__/shallowReactive.spec.ts +++ b/packages/reactivity/__tests__/shallowReactive.spec.ts @@ -160,6 +160,7 @@ describe('shallowReactive', () => { shallowArray.pop() expect(size).toBe(0) }) + test('should not observe when iterating', () => { const shallowArray = shallowReactive([]) const a = {} diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index a1b3003a5e7..ab2ed378129 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -11,13 +11,7 @@ import { toRaw, } from './reactive' 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, @@ -29,6 +23,7 @@ import { } from '@vue/shared' import { isRef } from './ref' import { warn } from './warning' +import { endBatch, pauseTracking, resetTracking, startBatch } from './effect' const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`) @@ -69,11 +64,11 @@ function createArrayInstrumentations() { // which leads to infinite loops in some cases (#2137) ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { + startBatch() pauseTracking() - pauseScheduling() const res = (toRaw(this) as any)[key].apply(this, args) - resetScheduling() resetTracking() + endBatch() return res } }) @@ -133,7 +128,14 @@ class BaseReactiveHandler implements ProxyHandler { } } - 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 diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 58e69b1cc62..2636287b610 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -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' diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index a4b74172fcf..3e0fce6ec02 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,10 +1,18 @@ -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 ReactiveEffect, + 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 +22,10 @@ export interface ComputedRef extends WritableComputedRef { } export interface WritableComputedRef extends Ref { - readonly effect: ReactiveEffect + /** + * @deprecated computed no longer uses effect + */ + effect: ReactiveEffect } export type ComputedGetter = (oldValue?: T) => T @@ -25,74 +36,71 @@ export interface WritableComputedOptions { set: ComputedSetter } -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` - -export class ComputedRefImpl { - public dep?: Dep = undefined - - private _value!: T - public readonly effect: ReactiveEffect - - public readonly __v_isRef = true - public readonly [ReactiveFlags.IS_READONLY]: boolean = false - - public _cacheable: boolean +/** + * @internal + */ +export class ComputedRefImpl implements Subscriber { + // A computed is a ref + _value: any = undefined + readonly dep = new Dep(this) + readonly __v_isRef = true; + readonly [ReactiveFlags.IS_READONLY]: boolean + // A computed is also a subscriber that tracks other deps + deps?: Link = undefined + depsTail?: Link = undefined + // track variaous states + flags = EffectFlags.DIRTY + // last seen global version + globalVersion = globalVersion - 1 + // for backwards compat + effect = this + + // dev only + onTrack?: (event: DebuggerEvent) => void + // dev only + onTrigger?: (event: DebuggerEvent) => void constructor( - getter: ComputedGetter, - private readonly _setter: ComputedSetter, - isReadonly: boolean, - isSSR: boolean, + public fn: ComputedGetter, + private readonly setter: ComputedSetter | undefined, + public 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 } - 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) + notify() { + // avoid infinite self recursion + if (activeSub !== this) { + this.flags |= EffectFlags.DIRTY + this.dep.notify() + } else if (__DEV__) { + // TODO warn } - trackRefValue(self) - if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) { - __DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN) - triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect) - } - 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 + get value() { + 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 + } + return this._value } - set _dirty(v) { - this.effect.dirty = v + set value(newValue) { + if (this.setter) { + this.setter(newValue) + } else if (__DEV__) { + warn('Write operation failed: computed value is readonly') + } } - // #endregion } /** @@ -142,26 +150,20 @@ export function computed( isSSR = false, ) { let getter: ComputedGetter - let setter: ComputedSetter + let setter: ComputedSetter | 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 diff --git a/packages/reactivity/src/deferredComputed.ts b/packages/reactivity/src/deferredComputed.ts deleted file mode 100644 index 1dbba1f3f03..00000000000 --- a/packages/reactivity/src/deferredComputed.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { computed } from './computed' - -/** - * @deprecated use `computed` instead. See #5912 - */ -export const deferredComputed = computed diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index c8e8a130dc9..5ba61d3a03f 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -1,17 +1,295 @@ -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 & { - cleanup: () => void - computed?: ComputedRefImpl +/** + * 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() + } + } +} + +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 +const targetMap = new WeakMap() + +export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') +export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map 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 | Set, +) { + 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 if (key === 'length' && isArray(target)) { + const newLength = Number(newValue) + depsMap.forEach((dep, key) => { + if (key === 'length' || (!isSymbol(key) && key >= newLength)) { + deps.push(dep) + } + }) + } else { + const push = (dep: Dep | undefined) => dep && deps.push(dep) + + // schedule runs for SET | ADD | DELETE + if (key !== void 0) { + push(depsMap.get(key)) + } + + // also run for iteration key on ADD | DELETE | Map.SET + switch (type) { + case TriggerOpTypes.ADD: + if (!isArray(target)) { + push(depsMap.get(ITERATE_KEY)) + if (isMap(target)) { + push(depsMap.get(MAP_KEY_ITERATE_KEY)) + } + } else if (isIntegerKey(key)) { + // new index added to array -> length changes + push(depsMap.get('length')) + } + break + case TriggerOpTypes.DELETE: + if (!isArray(target)) { + push(depsMap.get(ITERATE_KEY)) + if (isMap(target)) { + push(depsMap.get(MAP_KEY_ITERATE_KEY)) + } + } + break + case TriggerOpTypes.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() } -export const createDep = ( - cleanup: () => void, - computed?: ComputedRefImpl, -): Dep => { - const dep = new Map() as Dep - dep.cleanup = cleanup - dep.computed = computed - return dep +/** + * Test only + */ +export function getDepFromReactive(object: any, key: string | number | symbol) { + return targetMap.get(object)?.get(key) } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index ca90544c0de..5a4d05268dc 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -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 { recordEffectScope } from './effectScope' +import { warn } from './warning' export type EffectScheduler = (...args: any[]) => any export type DebuggerEvent = { - effect: ReactiveEffect + effect: Subscriber } & DebuggerEventExtraInfo export type DebuggerEventExtraInfo = { @@ -23,156 +20,398 @@ export type DebuggerEventExtraInfo = { oldTarget?: Map | Set } -export let activeEffect: ReactiveEffect | undefined +export interface DebuggerOptions { + onTrack?: (event: DebuggerEvent) => void + onTrigger?: (event: DebuggerEvent) => void +} -export class ReactiveEffect { - active = true - deps: Dep[] = [] +export interface ReactiveEffectOptions extends DebuggerOptions { + scheduler?: EffectScheduler + allowRecurse?: boolean + onStop?: () => void +} +export interface ReactiveEffectRunner { + (): T + 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, +} + +/** + * Subscriber is a type that tracks (or subscribes to) a list of deps. + */ +export interface Subscriber extends DebuggerOptions { /** - * Can be attached after creation + * Head of the doubly linked list representing the deps * @internal */ - computed?: ComputedRefImpl + deps?: Link /** + * Tail of the same list * @internal */ - allowRecurse?: boolean + depsTail?: Link + /** + * @internal + */ + flags: EffectFlags + /** + * @internal + */ + notify(): void +} - onStop?: () => void - // dev only - onTrack?: (event: DebuggerEvent) => void - // dev only - onTrigger?: (event: DebuggerEvent) => 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 + implements Subscriber, ReactiveEffectOptions +{ /** * @internal */ - _dirtyLevel = DirtyLevels.Dirty + deps?: Link = undefined /** * @internal */ - _trackId = 0 + depsTail?: Link = undefined /** * @internal */ - _runnings = 0 + flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING /** * @internal */ - _shouldSchedule = false + nextEffect?: ReactiveEffect = undefined /** * @internal */ - _depsLength = 0 + allowRecurse?: boolean - constructor( - public fn: () => T, - public trigger: () => void, - public scheduler?: EffectScheduler, - scope?: EffectScope, - ) { - recordEffectScope(this, scope) - } + scheduler?: EffectScheduler = undefined + onStop?: () => void + onTrack?: (event: DebuggerEvent) => void + onTrigger?: (event: DebuggerEvent) => void - 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 + constructor(public fn: () => T) { + recordEffectScope(this) } - public set dirty(v) { - this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty + /** + * @internal + */ + notify() { + if (this.flags & EffectFlags.RUNNING && !this.allowRecurse) { + 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() { - this._dirtyLevel = DirtyLevels.NotDirty - if (!this.active) { + // TODO cleanupEffect + + if (!(this.flags & EffectFlags.ACTIVE)) { + // stopped during cleanup return this.fn() } - let lastShouldTrack = shouldTrack - let lastEffect = activeEffect + + this.flags |= EffectFlags.RUNNING + prepareDeps(this) + const prevEffect = activeSub + const prevShouldTrack = shouldTrack + activeSub = this + shouldTrack = true + try { - shouldTrack = true - activeEffect = this - this._runnings++ - preCleanupEffect(this) return this.fn() } finally { - postCleanupEffect(this) - this._runnings-- - activeEffect = lastEffect - shouldTrack = lastShouldTrack + 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.active) { - preCleanupEffect(this) - postCleanupEffect(this) - this.onStop?.() - this.active = false + if (this.flags & EffectFlags.ACTIVE) { + for (let link = this.deps; link; link = link.nextDep) { + removeSub(link) + } + this.deps = this.depsTail = undefined + 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 triggerComputed(computed: ComputedRefImpl) { - return computed.value +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 preCleanupEffect(effect: ReactiveEffect) { - effect._trackId++ - effect._depsLength = 0 +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 postCleanupEffect(effect: ReactiveEffect) { - if (effect.deps.length > effect._depsLength) { - for (let i = effect._depsLength; i < effect.deps.length; i++) { - cleanupDepEffect(effect.deps[i], effect) +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 } - effect.deps.length = effect._depsLength } + // @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 } -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() +/** + * 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 } -export interface DebuggerOptions { - onTrack?: (event: DebuggerEvent) => void - onTrigger?: (event: DebuggerEvent) => void +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) + } + } } -export interface ReactiveEffectOptions extends DebuggerOptions { - lazy?: boolean - scheduler?: EffectScheduler - scope?: EffectScope - allowRecurse?: boolean - onStop?: () => void +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 { @@ -180,38 +419,26 @@ export interface ReactiveEffectRunner { effect: ReactiveEffect } -/** - * 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. - */ export function effect( fn: () => T, options?: ReactiveEffectOptions, -): ReactiveEffectRunner { +): ReactiveEffectRunner { 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 +451,10 @@ export function stop(runner: ReactiveEffectRunner) { runner.effect.stop() } +/** + * @internal + */ export let shouldTrack = true -export let pauseScheduleStack = 0 - const trackStack: boolean[] = [] /** @@ -252,76 +480,3 @@ export function resetTracking() { const last = trackStack.pop() shouldTrack = last === undefined ? true : last } - -export function pauseScheduling() { - pauseScheduleStack++ -} - -export function resetScheduling() { - pauseScheduleStack-- - while (!pauseScheduleStack && queueEffectSchedulers.length) { - queueEffectSchedulers.shift()!() - } -} - -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!)) - } - } -} - -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() -} diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 1c80fbc752b..40bdf7b1b04 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -44,16 +44,14 @@ export { type ComputedGetter, type ComputedSetter, } from './computed' -export { deferredComputed } from './deferredComputed' export { effect, stop, enableTracking, pauseTracking, resetTracking, - pauseScheduling, - resetScheduling, ReactiveEffect, + EffectFlags, type ReactiveEffectRunner, type ReactiveEffectOptions, type EffectScheduler, @@ -61,7 +59,7 @@ export { type DebuggerEvent, type DebuggerEventExtraInfo, } from './effect' -export { trigger, track, ITERATE_KEY } from './reactiveEffect' +export { trigger, track, ITERATE_KEY } from './dep' export { effectScope, EffectScope, diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts deleted file mode 100644 index 6bf0e75115a..00000000000 --- a/packages/reactivity/src/reactiveEffect.ts +++ /dev/null @@ -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 -const targetMap = new WeakMap() - -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 | Set, -) { - 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) -} diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 1b9d60ef06b..bfde3b787e3 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -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' declare const RefSymbol: unique symbol export declare const RawSymbol: unique symbol @@ -39,54 +31,6 @@ export interface Ref { [RefSymbol]: true } -type RefBase = { - dep?: Dep - value: T -} - -export function trackRefValue(ref: RefBase) { - 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, - 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. * @@ -95,7 +39,7 @@ export function triggerRefValue( */ export function isRef(r: Ref | unknown): r is Ref export function isRef(r: any): r is Ref { - return !!(r && r.__v_isRef === true) + return r ? r.__v_isRef === true : false } /** @@ -151,11 +95,15 @@ function createRef(rawValue: unknown, shallow: boolean) { return new RefImpl(rawValue, shallow) } -class RefImpl { - private _value: T +/** + * @internal + */ +class RefImpl { + _value: T private _rawValue: T - public dep?: Dep = undefined + dep: Dep = new Dep() + public readonly __v_isRef = true constructor( @@ -167,18 +115,37 @@ class RefImpl { } 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() + } } } } @@ -209,7 +176,16 @@ class RefImpl { * @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 | Ref @@ -295,7 +271,7 @@ export type CustomRefFactory = ( } class CustomRefImpl { - public dep?: Dep = undefined + public dep: Dep private readonly _get: ReturnType>['get'] private readonly _set: ReturnType>['set'] @@ -303,10 +279,8 @@ class CustomRefImpl { public readonly __v_isRef = true constructor(factory: CustomRefFactory) { - 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 } diff --git a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts index 04e9c1c86db..30c8951f405 100644 --- a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts +++ b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts @@ -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 `, + { + propsDestructure: false, + }, ) expect(content).toMatch(`const { foo } = __props`) assertCode(content) }) + test('prohibiting reactive destructure', () => { + expect(() => + compile( + ``, + { + propsDestructure: 'error', + }, + ), + ).toThrow() + }) + describe('errors', () => { test('w/ both type and non-type args', () => { expect(() => { diff --git a/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts index 3843ef92190..20f2c432d94 100644 --- a/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts @@ -6,7 +6,6 @@ describe('sfc reactive props destructure', () => { function compile(src: string, options?: Partial) { return compileSFCScript(src, { inlineTemplate: true, - propsDestructure: true, ...options, }) } diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index d4131d5c61d..41f083155d6 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -106,10 +106,11 @@ export interface SFCScriptCompileOptions { */ hoistStatic?: boolean /** - * (**Experimental**) Enable reactive destructure for `defineProps` - * @default false + * Set to `false` to disable reactive destructure for `defineProps` (pre-3.5 + * behavior), or set to `'error'` to throw hard error on props destructures. + * @default true */ - propsDestructure?: boolean + propsDestructure?: boolean | 'error' /** * File system access methods to be used when resolving types * imported in SFC macros. Defaults to ts.sys in Node.js, can be overwritten diff --git a/packages/compiler-sfc/src/script/definePropsDestructure.ts b/packages/compiler-sfc/src/script/definePropsDestructure.ts index e4a59aca7d5..34bc7a42818 100644 --- a/packages/compiler-sfc/src/script/definePropsDestructure.ts +++ b/packages/compiler-sfc/src/script/definePropsDestructure.ts @@ -22,23 +22,17 @@ import { genPropsAccessExp } from '@vue/shared' import { isCallOf, resolveObjectKey } from './utils' import type { ScriptCompileContext } from './context' import { DEFINE_PROPS } from './defineProps' -import { warnOnce } from '../warn' export function processPropsDestructure( ctx: ScriptCompileContext, declId: ObjectPattern, ) { - if (!ctx.options.propsDestructure) { + if (ctx.options.propsDestructure === 'error') { + ctx.error(`Props destructure is explicitly prohibited via config.`, declId) + } else if (ctx.options.propsDestructure === false) { return } - warnOnce( - `This project is using reactive props destructure, which is an experimental ` + - `feature. It may receive breaking changes or be removed in the future, so ` + - `use at your own risk.\n` + - `To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/502.`, - ) - ctx.propsDestructureDecl = declId const registerBinding = ( @@ -104,7 +98,7 @@ export function transformDestructuredProps( ctx: ScriptCompileContext, vueImportAliases: Record, ) { - if (!ctx.options.propsDestructure) { + if (ctx.options.propsDestructure === false) { return } From 5590ca3694fc98858951bcfa027fc92f899b05ae Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 29 Apr 2024 10:49:53 +0800 Subject: [PATCH 016/242] release: v3.5.0-alpha.1 --- CHANGELOG.md | 23 +++++++++++++++++++++++ package.json | 2 +- packages/compiler-core/package.json | 2 +- packages/compiler-dom/package.json | 2 +- packages/compiler-sfc/package.json | 2 +- packages/compiler-ssr/package.json | 2 +- packages/reactivity/package.json | 2 +- packages/runtime-core/package.json | 2 +- packages/runtime-dom/package.json | 2 +- packages/server-renderer/package.json | 2 +- packages/shared/package.json | 2 +- packages/vue-compat/package.json | 2 +- packages/vue/package.json | 2 +- 13 files changed, 35 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe351268a40..032a5f3a0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# [3.5.0-alpha.1](https://github.com/vuejs/core/compare/v3.4.25...v3.5.0-alpha.1) (2024-04-29) + + +### Bug Fixes + +* **reactivity:** fix call sequence of ontrigger in effect ([#10501](https://github.com/vuejs/core/issues/10501)) ([28841fe](https://github.com/vuejs/core/commit/28841fee43a45c37905c2c1ed9ace23067539045)) + + +### Features + +* **compiler-sfc:** enable reactive props destructure by default ([d2dac0e](https://github.com/vuejs/core/commit/d2dac0e359c47d1ed0aa77eda488e76fd6466d2d)) +* **reactivity:** `onEffectCleanup` API ([2cc5615](https://github.com/vuejs/core/commit/2cc5615590de77126e8df46136de0240dbde5004)), closes [#10173](https://github.com/vuejs/core/issues/10173) +* **reactivity:** add failSilently argument for onScopeDispose ([9a936aa](https://github.com/vuejs/core/commit/9a936aaec489c79433a32791ecf5ddb1739a62bd)) +* **transition:** support directly nesting Teleport inside Transition ([#6548](https://github.com/vuejs/core/issues/6548)) ([0e6e3c7](https://github.com/vuejs/core/commit/0e6e3c7eb0e5320b7c1818e025cb4a490fede9c0)), closes [#5836](https://github.com/vuejs/core/issues/5836) +* **types:** provide internal options for directly using user types in language tools ([#10801](https://github.com/vuejs/core/issues/10801)) ([75c8cf6](https://github.com/vuejs/core/commit/75c8cf63a1ef30ac84f91282d66ad3f57c6612e9)) + + +### Performance Improvements + +* **reactivity:** optimize array tracking ([#9511](https://github.com/vuejs/core/issues/9511)) ([70196a4](https://github.com/vuejs/core/commit/70196a40cc078f50fcc1110c38c06fbcc70b205e)), closes [#4318](https://github.com/vuejs/core/issues/4318) + + + ## [3.4.25](https://github.com/vuejs/core/compare/v3.4.24...v3.4.25) (2024-04-24) diff --git a/package.json b/package.json index a91136c80d7..7bf2a2c147b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.4.25", + "version": "3.5.0-alpha.1", "packageManager": "pnpm@9.0.5", "type": "module", "scripts": { diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index 5a4b7f65331..97a11c37346 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index 113dc583c57..25581867157 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 58564d43603..141123eb9fb 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index cb20b174338..90c1941d381 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index e0e789cf10d..eeca0e0809b 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index ccdca5888d4..c4d9682e99c 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 152243cf34c..77df975e388 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index e8929a1af4c..f9d9690deef 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "@vue/server-renderer", "main": "index.js", "module": "dist/server-renderer.esm-bundler.js", diff --git a/packages/shared/package.json b/packages/shared/package.json index 51b192193b9..ef920cf105d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index 6426431251d..e5276c93844 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compat", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "Vue 3 compatibility build for Vue 2", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", diff --git a/packages/vue/package.json b/packages/vue/package.json index 413b523b121..12b7e570f33 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "3.4.25", + "version": "3.5.0-alpha.1", "description": "The progressive JavaScript framework for building modern web UI.", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", From 582a3a382b1adda565bac576b913a88d9e8d7a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20L=C3=BCnborg?= Date: Mon, 29 Apr 2024 12:47:56 +0200 Subject: [PATCH 017/242] feat(runtime-core): add app.onUnmount() for registering cleanup functions (#4619) close #4516 --- .../__tests__/apiCreateApp.spec.ts | 30 +++++++++++++++++++ packages/runtime-core/src/apiCreateApp.ts | 18 +++++++++++ packages/runtime-core/src/errorHandling.ts | 2 ++ 3 files changed, 50 insertions(+) diff --git a/packages/runtime-core/__tests__/apiCreateApp.spec.ts b/packages/runtime-core/__tests__/apiCreateApp.spec.ts index f6386339741..0d7c3380311 100644 --- a/packages/runtime-core/__tests__/apiCreateApp.spec.ts +++ b/packages/runtime-core/__tests__/apiCreateApp.spec.ts @@ -344,6 +344,36 @@ describe('api: createApp', () => { ).toHaveBeenWarnedTimes(1) }) + test('onUnmount', () => { + const cleanup = vi.fn().mockName('plugin cleanup') + const PluginA: Plugin = app => { + app.provide('foo', 1) + app.onUnmount(cleanup) + } + const PluginB: Plugin = { + install: (app, arg1, arg2) => { + app.provide('bar', arg1 + arg2) + app.onUnmount(cleanup) + }, + } + + const app = createApp({ + render: () => `Test`, + }) + app.use(PluginA) + app.use(PluginB) + + const root = nodeOps.createElement('div') + app.mount(root) + + //also can be added after mount + app.onUnmount(cleanup) + + app.unmount() + + expect(cleanup).toHaveBeenCalledTimes(3) + }) + test('config.errorHandler', () => { const error = new Error() const count = ref(0) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 65c10166de7..286eb2bcc8e 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -27,6 +27,7 @@ import { version } from '.' import { installAppCompatProperties } from './compat/global' import type { NormalizedPropsOptions } from './componentProps' import type { ObjectEmitsOptions } from './componentEmits' +import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling' import type { DefineComponent } from './apiDefineComponent' export interface App { @@ -50,6 +51,7 @@ export interface App { namespace?: boolean | ElementNamespace, ): ComponentPublicInstance unmount(): void + onUnmount(cb: () => void): void provide(key: InjectionKey | string, value: T): this /** @@ -214,6 +216,7 @@ export function createAppAPI( const context = createAppContext() const installedPlugins = new WeakSet() + const pluginCleanupFns: Array<() => any> = [] let isMounted = false @@ -366,8 +369,23 @@ export function createAppAPI( } }, + onUnmount(cleanupFn: () => void) { + if (__DEV__ && typeof cleanupFn !== 'function') { + warn( + `Expected function as first argument to app.onUnmount(), ` + + `but got ${typeof cleanupFn}`, + ) + } + pluginCleanupFns.push(cleanupFn) + }, + unmount() { if (isMounted) { + callWithAsyncErrorHandling( + pluginCleanupFns, + app._instance, + ErrorCodes.APP_UNMOUNT_CLEANUP, + ) render(null, app._container) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { app._instance = null diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 41c92cbd34a..beddea5f644 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -23,6 +23,7 @@ export enum ErrorCodes { FUNCTION_REF, ASYNC_COMPONENT_LOADER, SCHEDULER, + APP_UNMOUNT_CLEANUP, } export const ErrorTypeStrings: Record = { @@ -57,6 +58,7 @@ export const ErrorTypeStrings: Record = { [ErrorCodes.SCHEDULER]: 'scheduler flush. This is likely a Vue internals bug. ' + 'Please open an issue at https://github.com/vuejs/core .', + [ErrorCodes.APP_UNMOUNT_CLEANUP]: 'app unmount cleanup function', } export type ErrorTypes = LifecycleHooks | ErrorCodes From 124c4cac833a28ae9bc8edc576c1d0c7c41f5985 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 30 Apr 2024 08:26:39 -0700 Subject: [PATCH 018/242] fix(types): props in defineOptions type should be optional close #10841 --- packages/runtime-core/src/apiSetupHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index dbe27dde48e..71254d37171 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -210,7 +210,7 @@ export function defineOptions< /** * props should be defined via defineProps(). */ - props: never + props?: never /** * emits should be defined via defineEmits(). */ From c146186396d0c1a65423b8c9a21251c5a6467336 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 30 Apr 2024 10:09:06 -0700 Subject: [PATCH 019/242] fix(types): fix compat with generated types that rely on CreateComponentPublicInstance close #10842 --- packages/dts-test/defineComponent.test-d.tsx | 193 ++++++++++++++++++ .../runtime-core/src/apiDefineComponent.ts | 8 +- packages/runtime-core/src/componentOptions.ts | 14 +- .../src/componentPublicInstance.ts | 67 +++++- packages/runtime-core/src/index.ts | 1 + packages/runtime-dom/src/apiCustomElement.ts | 4 +- 6 files changed, 273 insertions(+), 14 deletions(-) diff --git a/packages/dts-test/defineComponent.test-d.tsx b/packages/dts-test/defineComponent.test-d.tsx index 077f1abc075..aa0cfb0ebab 100644 --- a/packages/dts-test/defineComponent.test-d.tsx +++ b/packages/dts-test/defineComponent.test-d.tsx @@ -1766,3 +1766,196 @@ defineComponent({ expectType(props.foo) }, }) + +import type * as vue from 'vue' + +interface ErrorMessageSlotProps { + message: string | undefined +} +/** + * #10842 + * component types generated by vue-tsc + * relying on legacy CreateComponentPublicInstance signature + */ +declare const ErrorMessage: { + new (...args: any[]): vue.CreateComponentPublicInstance< + Readonly< + vue.ExtractPropTypes<{ + as: { + type: StringConstructor + default: any + } + name: { + type: StringConstructor + required: true + } + }> + >, + () => + | VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + > + | vue.Slot + | VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + >[] + | { + default: () => VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + >[] + }, + unknown, + {}, + {}, + vue.ComponentOptionsMixin, + vue.ComponentOptionsMixin, + {}, + vue.VNodeProps & + vue.AllowedComponentProps & + vue.ComponentCustomProps & + Readonly< + vue.ExtractPropTypes<{ + as: { + type: StringConstructor + default: any + } + name: { + type: StringConstructor + required: true + } + }> + >, + { + as: string + }, + true, + {}, + {}, + { + P: {} + B: {} + D: {} + C: {} + M: {} + Defaults: {} + }, + Readonly< + vue.ExtractPropTypes<{ + as: { + type: StringConstructor + default: any + } + name: { + type: StringConstructor + required: true + } + }> + >, + () => + | VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + > + | vue.Slot + | VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + >[] + | { + default: () => VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + >[] + }, + {}, + {}, + {}, + { + as: string + } + > + __isFragment?: never + __isTeleport?: never + __isSuspense?: never +} & vue.ComponentOptionsBase< + Readonly< + vue.ExtractPropTypes<{ + as: { + type: StringConstructor + default: any + } + name: { + type: StringConstructor + required: true + } + }> + >, + () => + | VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + > + | vue.Slot + | VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + >[] + | { + default: () => VNode< + vue.RendererNode, + vue.RendererElement, + { + [key: string]: any + } + >[] + }, + unknown, + {}, + {}, + vue.ComponentOptionsMixin, + vue.ComponentOptionsMixin, + {}, + string, + { + as: string + }, + {}, + string, + {} +> & + vue.VNodeProps & + vue.AllowedComponentProps & + vue.ComponentCustomProps & + (new () => { + $slots: { + default: (arg: ErrorMessageSlotProps) => VNode[] + } + }) +; diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 7fce96586da..98e9ae7952c 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -31,7 +31,7 @@ import { extend, isFunction } from '@vue/shared' import type { VNodeProps } from './vnode' import type { ComponentPublicInstanceConstructor, - CreateComponentPublicInstance, + CreateComponentPublicInstanceWithMixins, } from './componentPublicInstance' import type { SlotsType } from './componentSlots' import type { Directive } from './directives' @@ -68,7 +68,7 @@ export type DefineComponent< Provide extends ComponentProvideOptions = ComponentProvideOptions, MakeDefaultsOptional extends boolean = true, > = ComponentPublicInstanceConstructor< - CreateComponentPublicInstance< + CreateComponentPublicInstanceWithMixins< Props, RawBindings, D, @@ -116,7 +116,7 @@ export type DefineSetupFnComponent< PP = PublicProps, > = new ( props: Props & PP, -) => CreateComponentPublicInstance< +) => CreateComponentPublicInstanceWithMixins< Props, {}, {}, @@ -240,7 +240,7 @@ export function defineComponent< Provide > & ThisType< - CreateComponentPublicInstance< + CreateComponentPublicInstanceWithMixins< ResolvedProps, SetupBindings, Data, diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index ac1841edee9..2ede4404266 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -62,7 +62,7 @@ import type { import type { Directive } from './directives' import { type ComponentPublicInstance, - type CreateComponentPublicInstance, + type CreateComponentPublicInstanceWithMixins, type IntersectionMixin, type UnwrapMixinsType, isReservedPrefix, @@ -263,7 +263,7 @@ export type ComponentOptions< Provide > & ThisType< - CreateComponentPublicInstance< + CreateComponentPublicInstanceWithMixins< {}, RawBindings, D, @@ -372,7 +372,7 @@ interface LegacyOptions< // since that leads to some sort of circular inference and breaks ThisType // for the entire component. data?: ( - this: CreateComponentPublicInstance< + this: CreateComponentPublicInstanceWithMixins< Props, {}, {}, @@ -381,7 +381,7 @@ interface LegacyOptions< Mixin, Extends >, - vm: CreateComponentPublicInstance< + vm: CreateComponentPublicInstanceWithMixins< Props, {}, {}, @@ -1125,7 +1125,7 @@ export type ComponentOptionsWithoutProps< */ __typeEmits?: TE } & ThisType< - CreateComponentPublicInstance< + CreateComponentPublicInstanceWithMixins< PE, RawBindings, D, @@ -1187,7 +1187,7 @@ export type ComponentOptionsWithArrayProps< > & { props: PropNames[] } & ThisType< - CreateComponentPublicInstance< + CreateComponentPublicInstanceWithMixins< Props, RawBindings, D, @@ -1250,7 +1250,7 @@ export type ComponentOptionsWithObjectProps< > & { props: PropsOptions & ThisType } & ThisType< - CreateComponentPublicInstance< + CreateComponentPublicInstanceWithMixins< Props, RawBindings, D, diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 864b9786efe..91a7ae8d6d0 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -150,7 +150,71 @@ export type ComponentPublicInstanceConstructor< new (...args: any[]): T } +/** + * @deprecated This is no longer used internally, but exported and relied on by + * existing library types generated by vue-tsc. + */ export type CreateComponentPublicInstance< + P = {}, + B = {}, + D = {}, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, + E extends EmitsOptions = {}, + PublicProps = P, + Defaults = {}, + MakeDefaultsOptional extends boolean = false, + I extends ComponentInjectOptions = {}, + S extends SlotsType = {}, + PublicMixin = IntersectionMixin & IntersectionMixin, + PublicP = UnwrapMixinsType & EnsureNonVoid

, + PublicB = UnwrapMixinsType & EnsureNonVoid, + PublicD = UnwrapMixinsType & EnsureNonVoid, + PublicC extends ComputedOptions = UnwrapMixinsType & + EnsureNonVoid, + PublicM extends MethodOptions = UnwrapMixinsType & + EnsureNonVoid, + PublicDefaults = UnwrapMixinsType & + EnsureNonVoid, +> = ComponentPublicInstance< + PublicP, + PublicB, + PublicD, + PublicC, + PublicM, + E, + PublicProps, + PublicDefaults, + MakeDefaultsOptional, + ComponentOptionsBase< + P, + B, + D, + C, + M, + Mixin, + Extends, + E, + string, + Defaults, + {}, + string, + S + >, + I, + S +> + +/** + * This is the same as `CreateComponentPublicInstance` but adds local components, + * global directives, exposed, and provide inference. + * It changes the arguments order so that we don't need to repeat mixin + * inference everywhere internally, but it has to be a new type to avoid + * breaking types that relies on previous arguments order (#10842) + */ +export type CreateComponentPublicInstanceWithMixins< P = {}, B = {}, D = {}, @@ -167,6 +231,8 @@ export type CreateComponentPublicInstance< LC extends Record = {}, Directives extends Record = {}, Exposed extends string = string, + Provide extends ComponentProvideOptions = ComponentProvideOptions, + // mixin inference PublicMixin = IntersectionMixin & IntersectionMixin, PublicP = UnwrapMixinsType & EnsureNonVoid

, PublicB = UnwrapMixinsType & EnsureNonVoid, @@ -177,7 +243,6 @@ export type CreateComponentPublicInstance< EnsureNonVoid, PublicDefaults = UnwrapMixinsType & EnsureNonVoid, - Provide extends ComponentProvideOptions = ComponentProvideOptions, > = ComponentPublicInstance< PublicP, PublicB, diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e4a9e53f29c..abdd39a9cd1 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -279,6 +279,7 @@ export type { ComponentPublicInstance, ComponentCustomProperties, CreateComponentPublicInstance, + CreateComponentPublicInstanceWithMixins, } from './componentPublicInstance' export type { Renderer, diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 01728466241..6363f16de7c 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -9,7 +9,7 @@ import { type ComponentProvideOptions, type ComputedOptions, type ConcreteComponent, - type CreateComponentPublicInstance, + type CreateComponentPublicInstanceWithMixins, type DefineComponent, type Directive, type EmitsOptions, @@ -97,7 +97,7 @@ export function defineCustomElement< Provide > & ThisType< - CreateComponentPublicInstance< + CreateComponentPublicInstanceWithMixins< Readonly, SetupBindings, Data, From 9b82005bf36150662f4802bd1260766b0254816b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Exbrayat?= Date: Fri, 3 May 2024 22:27:23 +0200 Subject: [PATCH 020/242] test: defineOptions dts tests (#10849) --- packages/dts-test/README.md | 2 +- packages/dts-test/setupHelpers.test-d.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/dts-test/README.md b/packages/dts-test/README.md index 6f1b1da1d0e..f75cc8b8d5a 100644 --- a/packages/dts-test/README.md +++ b/packages/dts-test/README.md @@ -4,4 +4,4 @@ Tests TypeScript types to ensure the types remain as expected. - This directory is included in the root `tsconfig.json`, where package imports are aliased to `src` directories, so in IDEs and the `pnpm check` script the types are validated against source code. -- When running `tsc` with `packages/dts-test/tsconfig.test.json`, packages are resolved using normal `node` resolution, so the types are validated against actual **built** types. This requires the types to be built first via `pnpm build-types`. +- When running `tsc` with `packages/dts-test/tsconfig.test.json`, packages are resolved using normal `node` resolution, so the types are validated against actual **built** types. This requires the types to be built first via `pnpm build-dts`. diff --git a/packages/dts-test/setupHelpers.test-d.ts b/packages/dts-test/setupHelpers.test-d.ts index 883ebe6b254..2cfc04bad1d 100644 --- a/packages/dts-test/setupHelpers.test-d.ts +++ b/packages/dts-test/setupHelpers.test-d.ts @@ -5,6 +5,7 @@ import { defineComponent, defineEmits, defineModel, + defineOptions, defineProps, defineSlots, toRefs, @@ -501,3 +502,21 @@ describe('toRefs w/ type declaration', () => { }>() expectType>(toRefs(props).file) }) + +describe('defineOptions', () => { + defineOptions({ + name: 'MyComponent', + inheritAttrs: true, + }) + + defineOptions({ + // @ts-expect-error props should be defined via defineProps() + props: ['props'], + // @ts-expect-error emits should be defined via defineEmits() + emits: ['emits'], + // @ts-expect-error slots should be defined via defineSlots() + slots: { default: 'default' }, + // @ts-expect-error expose should be defined via defineExpose() + expose: ['expose'], + }) +}) From eae0ccb8e0d2772e34e9f6c48ce8be9e9e990452 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 3 May 2024 16:22:13 -0700 Subject: [PATCH 021/242] chore: re-export deprecated component options types --- packages/runtime-core/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index abdd39a9cd1..fd36136dc75 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -269,6 +269,10 @@ export type { ComputedOptions, RuntimeCompilerOptions, ComponentInjectOptions, + // deprecated + ComponentOptionsWithoutProps, + ComponentOptionsWithArrayProps, + ComponentOptionsWithObjectProps, } from './componentOptions' export type { EmitsOptions, From 908f70adc06038d1ea253d96f4024367f4a7545d Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 3 May 2024 16:29:23 -0700 Subject: [PATCH 022/242] fix(types): fix app.component() typing with inline defineComponent close #10843 --- packages/dts-test/defineComponent.test-d.tsx | 14 ++++++++++++++ packages/runtime-core/src/apiCreateApp.ts | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/dts-test/defineComponent.test-d.tsx b/packages/dts-test/defineComponent.test-d.tsx index aa0cfb0ebab..4af81be1dec 100644 --- a/packages/dts-test/defineComponent.test-d.tsx +++ b/packages/dts-test/defineComponent.test-d.tsx @@ -1959,3 +1959,17 @@ declare const ErrorMessage: { } }) ; + +// #10843 +createApp({}).component( + 'SomeComponent', + defineComponent({ + props: { + title: String, + }, + setup(props) { + expectType(props.title) + return {} + }, + }), +) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 286eb2bcc8e..12e63211a49 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -42,7 +42,10 @@ export interface App { mixin(mixin: ComponentOptions): this component(name: string): Component | undefined - component(name: string, component: Component | DefineComponent): this + component( + name: string, + component: T, + ): this directive(name: string): Directive | undefined directive(name: string, directive: Directive): this mount( From 801666fdade7632716474147599ac65595109fb8 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 3 May 2024 16:57:47 -0700 Subject: [PATCH 023/242] chore: add internal flag to work around ts issue --- packages/runtime-core/src/directives.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index c6dce57c1b6..afc7d3c1d28 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -52,8 +52,12 @@ export type DirectiveHook< prevVNode: Prev, ) => void -export type SSRDirectiveHook = ( - binding: DirectiveBinding, +export type SSRDirectiveHook< + Value = any, + Modifiers extends string = string, + Arg extends string = string, +> = ( + binding: DirectiveBinding, vnode: VNode, ) => Data | undefined @@ -63,6 +67,12 @@ export interface ObjectDirective< Modifiers extends string = string, Arg extends string = string, > { + /** + * @internal without this, ts-expect-error in directives.test-d.ts somehow + * fails when running tsc, but passes in IDE and when testing against built + * dts. Could be a TS bug. + */ + __mod?: Modifiers created?: DirectiveHook beforeMount?: DirectiveHook mounted?: DirectiveHook @@ -82,7 +92,7 @@ export interface ObjectDirective< > beforeUnmount?: DirectiveHook unmounted?: DirectiveHook - getSSRProps?: SSRDirectiveHook + getSSRProps?: SSRDirectiveHook deep?: boolean } From b295cdf4e9c79573a937c21c62fd02bc722087fc Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 3 May 2024 17:03:13 -0700 Subject: [PATCH 024/242] release: v3.5.0-alpha.2 --- CHANGELOG.md | 16 ++++++++++++++++ package.json | 2 +- packages/compiler-core/package.json | 2 +- packages/compiler-dom/package.json | 2 +- packages/compiler-sfc/package.json | 2 +- packages/compiler-ssr/package.json | 2 +- packages/reactivity/package.json | 2 +- packages/runtime-core/package.json | 2 +- packages/runtime-dom/package.json | 2 +- packages/server-renderer/package.json | 2 +- packages/shared/package.json | 2 +- packages/vue-compat/package.json | 2 +- packages/vue/package.json | 2 +- 13 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbeb2bfc8cb..46840721807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# [3.5.0-alpha.2](https://github.com/vuejs/core/compare/v3.4.26...v3.5.0-alpha.2) (2024-05-04) + + +### Bug Fixes + +* **types:** fix app.component() typing with inline defineComponent ([908f70a](https://github.com/vuejs/core/commit/908f70adc06038d1ea253d96f4024367f4a7545d)), closes [#10843](https://github.com/vuejs/core/issues/10843) +* **types:** fix compat with generated types that rely on CreateComponentPublicInstance ([c146186](https://github.com/vuejs/core/commit/c146186396d0c1a65423b8c9a21251c5a6467336)), closes [#10842](https://github.com/vuejs/core/issues/10842) +* **types:** props in defineOptions type should be optional ([124c4ca](https://github.com/vuejs/core/commit/124c4cac833a28ae9bc8edc576c1d0c7c41f5985)), closes [#10841](https://github.com/vuejs/core/issues/10841) + + +### Features + +* **runtime-core:** add app.onUnmount() for registering cleanup functions ([#4619](https://github.com/vuejs/core/issues/4619)) ([582a3a3](https://github.com/vuejs/core/commit/582a3a382b1adda565bac576b913a88d9e8d7a9e)), closes [#4516](https://github.com/vuejs/core/issues/4516) + + + ## [3.4.26](https://github.com/vuejs/core/compare/v3.4.25...v3.4.26) (2024-04-29) diff --git a/package.json b/package.json index 38510be0b21..23282e991b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "packageManager": "pnpm@9.0.6", "type": "module", "scripts": { diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index 97a11c37346..d139d92c34a 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index 25581867157..7969a3df19d 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 141123eb9fb..148c51cf266 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index 90c1941d381..4445399db82 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index eeca0e0809b..7b50a205c2f 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index c4d9682e99c..9a07e5c54bf 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 77df975e388..28c2f60f81b 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index f9d9690deef..f0d2efde4a7 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "@vue/server-renderer", "main": "index.js", "module": "dist/server-renderer.esm-bundler.js", diff --git a/packages/shared/package.json b/packages/shared/package.json index ef920cf105d..2c95128a8e9 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index e5276c93844..3f4a042edcc 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compat", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "Vue 3 compatibility build for Vue 2", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", diff --git a/packages/vue/package.json b/packages/vue/package.json index 12b7e570f33..bb3660ef040 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "3.5.0-alpha.1", + "version": "3.5.0-alpha.2", "description": "The progressive JavaScript framework for building modern web UI.", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", From a501a85a7c910868e01a5c70a2abea4e9d9e87f3 Mon Sep 17 00:00:00 2001 From: 4xi-2000 <73146951+4xii@users.noreply.github.com> Date: Mon, 27 May 2024 17:21:54 +0800 Subject: [PATCH 025/242] feat(compiler-core): support `Symbol` global in template expressions (#9069) --- packages/shared/src/globalsAllowList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/globalsAllowList.ts b/packages/shared/src/globalsAllowList.ts index 210650e3e2b..ca2cba60159 100644 --- a/packages/shared/src/globalsAllowList.ts +++ b/packages/shared/src/globalsAllowList.ts @@ -3,7 +3,7 @@ import { makeMap } from './makeMap' const GLOBALS_ALLOWED = 'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' + 'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' + - 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error' + 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol' export const isGloballyAllowed = /*#__PURE__*/ makeMap(GLOBALS_ALLOWED) From 8708a7f1ef63f67791bf08b95f44f02e8714e266 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 30 May 2024 11:25:39 +0800 Subject: [PATCH 026/242] chore: fix lint --- packages/reactivity/src/dep.ts | 1 + packages/runtime-core/src/scheduler.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index f4e4fd9719a..57d70f3eade 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -329,5 +329,6 @@ export function trigger( * Test only */ export function getDepFromReactive(object: any, key: string | number | symbol) { + // eslint-disable-next-line return targetMap.get(object)?.get(key) } diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index e41b9e6a7cb..28ebef95eef 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -98,7 +98,7 @@ export function queueJob(job: SchedulerJob) { } 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) + job.id >= ((queue[queue.length - 1] && queue[queue.length - 1].id) || 0) ) { queue.push(job) } else { From 189573dcee2a16bd3ed36ff5589d43f535e5e733 Mon Sep 17 00:00:00 2001 From: bqy_fe <1743369777@qq.com> Date: Thu, 30 May 2024 11:26:17 +0800 Subject: [PATCH 027/242] feat(types): export more emit related types (#11017) Co-authored-by: Evan You --- packages/runtime-core/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index fd36136dc75..a69ef3bdf6f 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -278,6 +278,8 @@ export type { EmitsOptions, ObjectEmitsOptions, EmitsToProps, + ShortEmitsToObject, + EmitFn, } from './componentEmits' export type { ComponentPublicInstance, From f8eba75d0acaee1b25171c71bc988c69df7a772e Mon Sep 17 00:00:00 2001 From: Wick Date: Thu, 30 May 2024 19:09:04 +0800 Subject: [PATCH 028/242] chore(reactivity): change literal flag properties to enum flag properties (#10367) --- packages/reactivity/src/computed.ts | 6 +++--- packages/reactivity/src/constants.ts | 1 + packages/reactivity/src/ref.ts | 29 +++++++++++++------------- packages/shared/src/toDisplayString.ts | 3 ++- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index f3054648b0c..a25c066e739 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -47,11 +47,11 @@ export class ComputedRefImpl implements Subscriber { /** * @internal */ - readonly dep = new Dep(this) + readonly dep = new Dep(this); /** * @internal */ - readonly __v_isRef = true; + readonly [ReactiveFlags.IS_REF] = true; /** * @internal */ @@ -96,7 +96,7 @@ export class ComputedRefImpl implements Subscriber { private readonly setter: ComputedSetter | undefined, isSSR: boolean, ) { - this.__v_isReadonly = !setter + this[ReactiveFlags.IS_READONLY] = !setter this.isSSR = isSSR } diff --git a/packages/reactivity/src/constants.ts b/packages/reactivity/src/constants.ts index baa75d61644..8320de287f2 100644 --- a/packages/reactivity/src/constants.ts +++ b/packages/reactivity/src/constants.ts @@ -20,6 +20,7 @@ export enum ReactiveFlags { IS_READONLY = '__v_isReadonly', IS_SHALLOW = '__v_isShallow', RAW = '__v_raw', + IS_REF = '__v_isRef', } export enum DirtyLevels { diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 32f79217e7b..bbe613a8cbf 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -16,7 +16,7 @@ import { toReactive, } from './reactive' import type { ComputedRef } from './computed' -import { TrackOpTypes, TriggerOpTypes } from './constants' +import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { warn } from './warning' declare const RefSymbol: unique symbol @@ -40,7 +40,7 @@ export interface Ref { */ export function isRef(r: Ref | unknown): r is Ref export function isRef(r: any): r is Ref { - return r ? r.__v_isRef === true : false + return r ? r[ReactiveFlags.IS_REF] === true : false } /** @@ -105,14 +105,13 @@ class RefImpl { dep: Dep = new Dep() - public readonly __v_isRef = true + public readonly [ReactiveFlags.IS_REF] = true + public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false - constructor( - value: T, - public readonly __v_isShallow: boolean, - ) { - this._rawValue = __v_isShallow ? value : toRaw(value) - this._value = __v_isShallow ? value : toReactive(value) + constructor(value: T, isShallow: boolean) { + this._rawValue = isShallow ? value : toRaw(value) + this._value = isShallow ? value : toReactive(value) + this[ReactiveFlags.IS_SHALLOW] = isShallow } get value() { @@ -131,7 +130,9 @@ class RefImpl { set value(newValue) { const oldValue = this._rawValue const useDirectValue = - this.__v_isShallow || isShallow(newValue) || isReadonly(newValue) + this[ReactiveFlags.IS_SHALLOW] || + isShallow(newValue) || + isReadonly(newValue) newValue = useDirectValue ? newValue : toRaw(newValue) if (hasChanged(newValue, oldValue)) { this._rawValue = newValue @@ -277,7 +278,7 @@ class CustomRefImpl { private readonly _get: ReturnType>['get'] private readonly _set: ReturnType>['set'] - public readonly __v_isRef = true + public readonly [ReactiveFlags.IS_REF] = true constructor(factory: CustomRefFactory) { const dep = (this.dep = new Dep()) @@ -330,7 +331,7 @@ export function toRefs(object: T): ToRefs { } class ObjectRefImpl { - public readonly __v_isRef = true + public readonly [ReactiveFlags.IS_REF] = true constructor( private readonly _object: T, @@ -353,8 +354,8 @@ class ObjectRefImpl { } class GetterRefImpl { - public readonly __v_isRef = true - public readonly __v_isReadonly = true + public readonly [ReactiveFlags.IS_REF] = true + public readonly [ReactiveFlags.IS_READONLY] = true constructor(private readonly _getter: () => T) {} get value() { return this._getter() diff --git a/packages/shared/src/toDisplayString.ts b/packages/shared/src/toDisplayString.ts index b63cb4112a5..2ee406a9d97 100644 --- a/packages/shared/src/toDisplayString.ts +++ b/packages/shared/src/toDisplayString.ts @@ -1,3 +1,4 @@ +import { ReactiveFlags } from '@vue/reactivity' import { isArray, isFunction, @@ -28,7 +29,7 @@ export const toDisplayString = (val: unknown): string => { const replacer = (_key: string, val: any): any => { // can't use isRef here since @vue/shared has no deps - if (val && val.__v_isRef) { + if (val && val[ReactiveFlags.IS_REF]) { return replacer(_key, val.value) } else if (isMap(val)) { return { From cd0ea0d479a276583fa181d8ecbc97fb0e4a9dce Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 4 Jun 2024 20:09:54 +0800 Subject: [PATCH 029/242] fix(compiler-core): change node hoisting to caching per instance (#11067) close #5256 close #9219 close #10959 --- .../__snapshots__/parse.spec.ts.snap | 112 ++-- .../__snapshots__/scopeId.spec.ts.snap | 16 - .../compiler-core/__tests__/codegen.spec.ts | 6 +- .../compiler-core/__tests__/scopeId.spec.ts | 27 - ....spec.ts.snap => cacheStatic.spec.ts.snap} | 232 ++++---- ...oistStatic.spec.ts => cacheStatic.spec.ts} | 526 ++++++++++++------ .../__tests__/transforms/vModel.spec.ts | 6 +- .../__tests__/transforms/vOn.spec.ts | 22 +- .../__tests__/transforms/vOnce.spec.ts | 16 +- packages/compiler-core/src/ast.ts | 17 +- packages/compiler-core/src/codegen.ts | 41 +- packages/compiler-core/src/index.ts | 2 +- packages/compiler-core/src/options.ts | 2 +- packages/compiler-core/src/runtimeHelpers.ts | 7 + packages/compiler-core/src/transform.ts | 20 +- .../{hoistStatic.ts => cacheStatic.ts} | 150 +++-- .../src/transforms/transformElement.ts | 2 +- .../src/transforms/transformExpression.ts | 2 +- .../src/transforms/transformText.ts | 2 +- packages/compiler-core/src/transforms/vFor.ts | 4 +- packages/compiler-core/src/transforms/vIf.ts | 2 +- .../compiler-core/src/transforms/vMemo.ts | 4 +- .../compiler-core/src/transforms/vModel.ts | 2 +- packages/compiler-core/src/utils.ts | 9 +- .../stringifyStatic.spec.ts.snap | 124 +++-- .../transforms/stringifyStatic.spec.ts | 413 ++++++-------- .../__tests__/transforms/vOn.spec.ts | 2 +- .../src/transforms/stringifyStatic.ts | 90 +-- .../__snapshots__/compileScript.spec.ts.snap | 4 +- .../templateTransformAssetUrl.spec.ts.snap | 15 +- .../templateTransformSrcset.spec.ts.snap | 339 +++++------ .../runtime-core/__tests__/hydration.spec.ts | 2 +- packages/runtime-core/src/hydration.ts | 2 +- packages/runtime-core/src/vnode.ts | 6 +- packages/shared/src/patchFlags.ts | 6 +- 35 files changed, 1167 insertions(+), 1065 deletions(-) rename packages/compiler-core/__tests__/transforms/__snapshots__/{hoistStatic.spec.ts.snap => cacheStatic.spec.ts.snap} (67%) rename packages/compiler-core/__tests__/transforms/{hoistStatic.spec.ts => cacheStatic.spec.ts} (52%) rename packages/compiler-core/src/transforms/{hoistStatic.ts => cacheStatic.ts} (72%) diff --git a/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap index 678548e35b5..942eed4c4dc 100644 --- a/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap @@ -2,7 +2,7 @@ exports[`compiler: parse > Edge Cases > invalid html 1`] = ` { - "cached": 0, + "cached": [], "children": [ { "children": [ @@ -86,7 +86,7 @@ exports[`compiler: parse > Edge Cases > invalid html 1`] = ` exports[`compiler: parse > Edge Cases > self closing multiple tag 1`] = ` { - "cached": 0, + "cached": [], "children": [ { "children": [], @@ -280,7 +280,7 @@ exports[`compiler: parse > Edge Cases > self closing multiple tag 1`] = ` exports[`compiler: parse > Edge Cases > valid html 1`] = ` { - "cached": 0, + "cached": [], "children": [ { "children": [ @@ -498,7 +498,7 @@ exports[`compiler: parse > Edge Cases > valid html 1`] = ` exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > 1`] = ` { - "cached": 0, + "cached": [], "children": [ { "children": [], @@ -550,7 +550,7 @@ exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT >