From: Evan You Date: Fri, 14 Jun 2024 10:32:28 +0000 (+0200) Subject: chore: Merge branch 'main' into minor X-Git-Tag: v3.5.0-alpha.3~18 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=26356264d294f1d88edc718bc24697afd8ffdc3b;p=thirdparty%2Fvuejs%2Fcore.git chore: Merge branch 'main' into minor --- 26356264d294f1d88edc718bc24697afd8ffdc3b diff --cc package.json index d454391c70,6b6050d23a..4205f42fe9 --- a/package.json +++ b/package.json @@@ -1,7 -1,7 +1,7 @@@ { "private": true, - "version": "3.4.28", + "version": "3.5.0-alpha.2", - "packageManager": "pnpm@9.1.2", + "packageManager": "pnpm@9.2.0", "type": "module", "scripts": { "dev": "node scripts/dev.js", diff --cc packages/compiler-core/__tests__/transforms/vOn.spec.ts index 4a285e627b,27d5027533..66097f29f0 --- a/packages/compiler-core/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts @@@ -598,10 -599,21 +599,21 @@@ describe('compiler: transform v-on', ( cacheHandlers: true, }, ) -- expect(root.cached).not.toBe(2) - expect(root.cached).toBe(1) ++ expect(root.cached.length).not.toBe(2) + expect(root.cached.length).toBe(1) }) + test('unicode identifier should not be cached (v-for)', () => { + const { root } = parseWithVOn( + `
`, + { + prefixIdentifiers: true, + cacheHandlers: true, + }, + ) - expect(root.cached).toBe(0) ++ expect(root.cached.length).toBe(0) + }) + test('inline function expression handler', () => { const { root, node } = parseWithVOn(`
`, { prefixIdentifiers: true, diff --cc packages/reactivity/__tests__/computed.spec.ts index e2325be54d,10c09109fd..dc3df3eaae --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@@ -577,279 -615,32 +577,304 @@@ 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']) }) + + it('debug: onTrigger (ref)', () => { + let events: DebuggerEvent[] = [] + const onTrigger = vi.fn((e: DebuggerEvent) => { + events.push(e) + }) + const obj = ref(1) + const c = computed(() => obj.value, { onTrigger }) + - // computed won't trigger compute until accessed - c.value ++ // computed won't track until it has a subscriber ++ effect(() => c.value) + + obj.value++ + + 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: 'value', + oldValue: 1, + newValue: 2, + }) + }) }) diff --cc packages/reactivity/src/ref.ts index bbe613a8cb,e47b8aa558..48ada16037 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@@ -5,9 -13,7 +5,10 @@@ import isFunction, isObject, } from '@vue/shared' +import { Dep, getDepFromReactive } from './dep' import { ++ type Builtin, + type ShallowReactiveMarker, isProxy, isReactive, isReadonly, diff --cc packages/runtime-core/src/apiCreateApp.ts index 12e63211a4,b3373dc866..3b6c047f3c --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@@ -54,8 -50,10 +54,11 @@@ export interface App void): void - provide(key: InjectionKey | string, value: T): this + provide | string | number>( + key: K, + value: K extends InjectionKey ? V : T, + ): this /** * Runs a function with the app as active instance. This allows using of `inject()` within the function to get access diff --cc packages/runtime-core/src/componentPublicInstance.ts index d7cfc5bea2,fd227774d5..41fce67d0c --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@@ -1,8 -1,7 +1,8 @@@ import { + type Component, type ComponentInternalInstance, type Data, - getExposeProxy, + getComponentPublicInstance, isStatefulComponent, } from './component' import { nextTick, queueJob } from './scheduler' diff --cc packages/runtime-core/src/directives.ts index afc7d3c1d2,b2618c03a6..c6fd282909 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@@ -26,29 -26,19 +26,29 @@@ import { mapCompatDirectiveHook } from import { pauseTracking, resetTracking } from '@vue/reactivity' import { traverse } from './apiWatch' -export interface DirectiveBinding { +export interface DirectiveBinding< + Value = any, + Modifiers extends string = string, + Arg extends string = string, +> { - instance: ComponentPublicInstance | null + instance: ComponentPublicInstance | Record | null - value: V - oldValue: V | null - arg?: string - modifiers: DirectiveModifiers - dir: ObjectDirective + value: Value + oldValue: Value | null + arg?: Arg + modifiers: DirectiveModifiers + dir: ObjectDirective } -export type DirectiveHook | null, V = any> = ( - el: T, - binding: DirectiveBinding, - vnode: VNode, +export type DirectiveHook< + HostElement = any, + Prev = VNode | null, + Value = any, + Modifiers extends string = string, + Arg extends string = string, +> = ( + el: HostElement, + binding: DirectiveBinding, + vnode: VNode, prevVNode: Prev, ) => void diff --cc packages/runtime-core/src/hmr.ts index 08f861f6b1,8196eb8919..56aa3c64b1 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@@ -137,7 -138,12 +137,11 @@@ function reload(id: string, newComp: HM // 4. Force the parent instance to re-render. This will cause all updated // components to be unmounted and re-mounted. Queue the update so that we // don't end up forcing the same parent to re-render multiple times. - queueJob(instance.parent.update) - instance.parent.effect.dirty = true + queueJob(() => { + instance.parent!.update() + // #6930 avoid infinite recursion + hmrDirtyComponents.delete(oldComp) + }) } else if (instance.appContext.reload) { // root instance mounted via createApp() has a reload method instance.appContext.reload() diff --cc packages/runtime-core/src/renderer.ts index 0bba1bcb0e,ccb89085c4..49bfd6b8ba --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@@ -2266,7 -2273,9 +2274,9 @@@ function baseCreateRenderer unregisterHMR(instance) } - const { bum, scope, job, subTree, um } = instance - const { bum, scope, update, subTree, um, m, a } = instance ++ const { bum, scope, job, subTree, um, m, a } = instance + invalidateMount(m) + invalidateMount(a) // beforeUnmount hook if (bum) { @@@ -2539,3 -2543,9 +2550,10 @@@ function locateNonHydratedAsyncRoot } } } + + export function invalidateMount(hooks: LifecycleHook) { + if (hooks) { - for (let i = 0; i < hooks.length; i++) hooks[i].active = false ++ for (let i = 0; i < hooks.length; i++) ++ hooks[i].flags! |= SchedulerJobFlags.DISPOSED + } + } diff --cc packages/runtime-core/src/scheduler.ts index 28ebef95ee,4ae1c6d46e..1c42b1779e --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@@ -192,14 -185,11 +192,12 @@@ export function flushPostFlushCbs(seen? postFlushIndex < activePostFlushCbs.length; postFlushIndex++ ) { - if ( - __DEV__ && - checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex]) - ) { + const cb = activePostFlushCbs[postFlushIndex] + if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { continue } - activePostFlushCbs[postFlushIndex]() - activePostFlushCbs[postFlushIndex].flags! &= ~SchedulerJobFlags.QUEUED - if (cb.active !== false) cb() ++ if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) cb() ++ cb.flags! &= ~SchedulerJobFlags.QUEUED } activePostFlushCbs = null postFlushIndex = 0