diff --git a/CHANGELOG.md b/CHANGELOG.md index 577fb0efd2e..28a30bfa8c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## [3.5.2](https://github.com/vuejs/core/compare/v3.5.1...v3.5.2) (2024-09-05) + + +### Bug Fixes + +* **reactivity:** make toRaw work on proxies created by proxyRef ([46c3ab1](https://github.com/vuejs/core/commit/46c3ab1d714024894fa1d33e495d5d35c7817d4d)) +* **reactivity:** pass oldValue to computed getter ([#11813](https://github.com/vuejs/core/issues/11813)) ([98864a7](https://github.com/vuejs/core/commit/98864a7ef5c8080c407166c8221488a4eacbbc81)), closes [#11812](https://github.com/vuejs/core/issues/11812) +* **reactivity:** prevent endless recursion in computed getters ([#11797](https://github.com/vuejs/core/issues/11797)) ([716275d](https://github.com/vuejs/core/commit/716275d1b1d2383d8ef0306fcd94558d4d9170f2)) +* **reactivity:** self-referencing computed should refresh ([e84c4a6](https://github.com/vuejs/core/commit/e84c4a608e9dc96fb2a4a29d538bcc64f26103a2)), closes [/github.com/vuejs/core/pull/11797#issuecomment-2330738633](https://github.com//github.com/vuejs/core/pull/11797/issues/issuecomment-2330738633) +* **scheduler:** prevent duplicate jobs being queued ([#11826](https://github.com/vuejs/core/issues/11826)) ([df56cc5](https://github.com/vuejs/core/commit/df56cc528793b1d6131a1e64095dd5cb95c56bee)), closes [#11712](https://github.com/vuejs/core/issues/11712) [#11807](https://github.com/vuejs/core/issues/11807) +* **suspense:** avoid updating anchor if activeBranch has not been rendered to the actual container ([#11818](https://github.com/vuejs/core/issues/11818)) ([3c0d531](https://github.com/vuejs/core/commit/3c0d531fa7fe762bfe46fbe63f318adc95221795)), closes [#11806](https://github.com/vuejs/core/issues/11806) +* **Transition:** handle KeepAlive child unmount in Transition out-in mode ([#11778](https://github.com/vuejs/core/issues/11778)) ([3116553](https://github.com/vuejs/core/commit/311655352931863dfcf520b8cf29cebc5b7e1e00)), closes [#11775](https://github.com/vuejs/core/issues/11775) +* **types:** add HTMLDialogElement missing close event ([#11811](https://github.com/vuejs/core/issues/11811)) ([3634f7a](https://github.com/vuejs/core/commit/3634f7a4c1649ad2e7e969eb4512512868c61d01)) +* **types:** added name attribute support to details tag ([#11823](https://github.com/vuejs/core/issues/11823)) ([c74176e](https://github.com/vuejs/core/commit/c74176ec7b4d1d34159ce21d600c04b157ac5549)), closes [#11821](https://github.com/vuejs/core/issues/11821) +* **types:** fix defineComponent props inference when setup() has explicit annotation ([fca20a3](https://github.com/vuejs/core/commit/fca20a39aa4a6f98c8f972bd435ebb7dc535648a)), closes [#11803](https://github.com/vuejs/core/issues/11803) +* **useTemplateRef:** properly fix readonly warning in dev and ensure prod behavior consistency ([9b7797d](https://github.com/vuejs/core/commit/9b7797d0d1fc773e979e042673d5b9b3151c40fc)), closes [#11808](https://github.com/vuejs/core/issues/11808) [#11816](https://github.com/vuejs/core/issues/11816) [#11810](https://github.com/vuejs/core/issues/11810) + + +### Features + +* **compiler-core:** parse modifiers as expression to provide location data ([#11819](https://github.com/vuejs/core/issues/11819)) ([3f13203](https://github.com/vuejs/core/commit/3f13203564164eeb2945bdc0b9ef755c37477d75)) + + + ## [3.5.1](https://github.com/vuejs/core/compare/v3.5.0...v3.5.1) (2024-09-04) diff --git a/package.json b/package.json index 100997cadb9..3090d82b669 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.5.1", + "version": "3.5.2", "packageManager": "pnpm@9.9.0", "type": "module", "scripts": { diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 0124a8b1525..9b4c184719c 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -1041,6 +1041,18 @@ describe('emits', () => { }, }) + // #11803 manual props annotation in setup() + const Hello = defineComponent({ + name: 'HelloWorld', + inheritAttrs: false, + props: { foo: String }, + emits: { + customClick: (args: string) => typeof args === 'string', + }, + setup(props: { foo?: string }) {}, + }) + ; {}} /> + // without emits defineComponent({ setup(props, { emit }) { @@ -1810,6 +1822,15 @@ describe('__typeRefs backdoor, object syntax', () => { expectType(refs.child.$refs.foo) }) +describe('__typeEl backdoor', () => { + const Comp = defineComponent({ + __typeEl: {} as HTMLAnchorElement, + }) + const c = new Comp() + + expectType(c.$el) +}) + defineComponent({ props: { foo: [String, null], diff --git a/packages-private/dts-test/tsx.test-d.tsx b/packages-private/dts-test/tsx.test-d.tsx index 63945c0629b..0cd380f0447 100644 --- a/packages-private/dts-test/tsx.test-d.tsx +++ b/packages-private/dts-test/tsx.test-d.tsx @@ -121,3 +121,5 @@ expectType( xmlns="http://www.w3.org/2000/svg" />, ) +// details +expectType(
) diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index 22fb209cfb9..37e81e64924 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -1358,7 +1358,27 @@ describe('compiler: parse', () => { name: 'on', rawName: 'v-on.enter', arg: undefined, - modifiers: ['enter'], + modifiers: [ + { + constType: 3, + content: 'enter', + isStatic: true, + loc: { + end: { + column: 16, + line: 1, + offset: 15, + }, + source: 'enter', + start: { + column: 11, + line: 1, + offset: 10, + }, + }, + type: 4, + }, + ], exp: undefined, loc: { start: { offset: 5, line: 1, column: 6 }, @@ -1377,7 +1397,46 @@ describe('compiler: parse', () => { name: 'on', rawName: 'v-on.enter.exact', arg: undefined, - modifiers: ['enter', 'exact'], + modifiers: [ + { + constType: 3, + content: 'enter', + isStatic: true, + loc: { + end: { + column: 16, + line: 1, + offset: 15, + }, + source: 'enter', + start: { + column: 11, + line: 1, + offset: 10, + }, + }, + type: 4, + }, + { + constType: 3, + content: 'exact', + isStatic: true, + loc: { + end: { + column: 22, + line: 1, + offset: 21, + }, + source: 'exact', + start: { + column: 17, + line: 1, + offset: 16, + }, + }, + type: 4, + }, + ], exp: undefined, loc: { start: { offset: 5, line: 1, column: 6 }, @@ -1406,7 +1465,46 @@ describe('compiler: parse', () => { source: 'click', }, }, - modifiers: ['enter', 'exact'], + modifiers: [ + { + constType: 3, + content: 'enter', + isStatic: true, + loc: { + end: { + column: 22, + line: 1, + offset: 21, + }, + source: 'enter', + start: { + column: 17, + line: 1, + offset: 16, + }, + }, + type: 4, + }, + { + constType: 3, + content: 'exact', + isStatic: true, + loc: { + end: { + column: 28, + line: 1, + offset: 27, + }, + source: 'exact', + start: { + column: 23, + line: 1, + offset: 22, + }, + }, + type: 4, + }, + ], exp: undefined, loc: { start: { offset: 5, line: 1, column: 6 }, @@ -1435,7 +1533,27 @@ describe('compiler: parse', () => { source: '[a.b]', }, }, - modifiers: ['camel'], + modifiers: [ + { + constType: 3, + content: 'camel', + isStatic: true, + loc: { + end: { + column: 22, + line: 1, + offset: 21, + }, + source: 'camel', + start: { + column: 17, + line: 1, + offset: 16, + }, + }, + type: 4, + }, + ], exp: undefined, loc: { start: { offset: 5, line: 1, column: 6 }, @@ -1530,7 +1648,27 @@ describe('compiler: parse', () => { source: 'a', }, }, - modifiers: ['prop'], + modifiers: [ + { + constType: 0, + content: 'prop', + isStatic: false, + loc: { + end: { + column: 1, + line: 1, + offset: 0, + }, + source: '', + start: { + column: 1, + line: 1, + offset: 0, + }, + }, + type: 4, + }, + ], exp: { type: NodeTypes.SIMPLE_EXPRESSION, content: 'b', @@ -1569,7 +1707,27 @@ describe('compiler: parse', () => { source: 'a', }, }, - modifiers: ['sync'], + modifiers: [ + { + constType: 3, + content: 'sync', + isStatic: true, + loc: { + end: { + column: 13, + line: 1, + offset: 12, + }, + source: 'sync', + start: { + column: 9, + line: 1, + offset: 8, + }, + }, + type: 4, + }, + ], exp: { type: NodeTypes.SIMPLE_EXPRESSION, content: 'b', @@ -1649,7 +1807,27 @@ describe('compiler: parse', () => { source: 'a', }, }, - modifiers: ['enter'], + modifiers: [ + { + constType: 3, + content: 'enter', + isStatic: true, + loc: { + end: { + column: 14, + line: 1, + offset: 13, + }, + source: 'enter', + start: { + column: 9, + line: 1, + offset: 8, + }, + }, + type: 4, + }, + ], exp: { type: NodeTypes.SIMPLE_EXPRESSION, content: 'b', diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index b474c8bd61e..6f1ba3f8986 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.5.1", + "version": "3.5.2", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 8116f532b79..cfd5fee2569 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -203,7 +203,7 @@ export interface DirectiveNode extends Node { rawName?: string exp: ExpressionNode | undefined arg: ExpressionNode | undefined - modifiers: string[] + modifiers: SimpleExpressionNode[] /** * optional property to cache the expression parse result for v-for */ diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts index cac943dd63d..304807d076a 100644 --- a/packages/compiler-core/src/parser.ts +++ b/packages/compiler-core/src/parser.ts @@ -225,7 +225,7 @@ const tokenizer = new Tokenizer(stack, { rawName: raw, exp: undefined, arg: undefined, - modifiers: raw === '.' ? ['prop'] : [], + modifiers: raw === '.' ? [createSimpleExpression('prop')] : [], loc: getLoc(start), } if (name === 'pre') { @@ -273,7 +273,8 @@ const tokenizer = new Tokenizer(stack, { setLocEnd(arg.loc, end) } } else { - ;(currentProp as DirectiveNode).modifiers.push(mod) + const exp = createSimpleExpression(mod, true, getLoc(start, end)) + ;(currentProp as DirectiveNode).modifiers.push(exp) } }, @@ -379,7 +380,9 @@ const tokenizer = new Tokenizer(stack, { if ( __COMPAT__ && currentProp.name === 'bind' && - (syncIndex = currentProp.modifiers.indexOf('sync')) > -1 && + (syncIndex = currentProp.modifiers.findIndex( + mod => mod.content === 'sync', + )) > -1 && checkCompatEnabled( CompilerDeprecationTypes.COMPILER_V_BIND_SYNC, currentOptions, diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index c917436ea91..76ca1d44353 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -665,7 +665,7 @@ export function buildProps( } // force hydration for v-bind with .prop modifier - if (isVBind && modifiers.includes('prop')) { + if (isVBind && modifiers.some(mod => mod.content === 'prop')) { patchFlag |= PatchFlags.NEED_HYDRATION } diff --git a/packages/compiler-core/src/transforms/vBind.ts b/packages/compiler-core/src/transforms/vBind.ts index 76a36145c06..233ed1e7e86 100644 --- a/packages/compiler-core/src/transforms/vBind.ts +++ b/packages/compiler-core/src/transforms/vBind.ts @@ -69,7 +69,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => { } // .sync is replaced by v-model:arg - if (modifiers.includes('camel')) { + if (modifiers.some(mod => mod.content === 'camel')) { if (arg.type === NodeTypes.SIMPLE_EXPRESSION) { if (arg.isStatic) { arg.content = camelize(arg.content) @@ -83,10 +83,10 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => { } if (!context.inSSR) { - if (modifiers.includes('prop')) { + if (modifiers.some(mod => mod.content === 'prop')) { injectPrefix(arg, '.') } - if (modifiers.includes('attr')) { + if (modifiers.some(mod => mod.content === 'attr')) { injectPrefix(arg, '^') } } diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index 60c7dc63bd1..f168c181803 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/packages/compiler-core/src/transforms/vModel.ts @@ -131,6 +131,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { // modelModifiers: { foo: true, "bar-baz": true } if (dir.modifiers.length && node.tagType === ElementTypes.COMPONENT) { const modifiers = dir.modifiers + .map(m => m.content) .map(m => (isSimpleIdentifier(m) ? m : JSON.stringify(m)) + `: true`) .join(`, `) const modifiersKey = arg diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index c38c22c6663..66e8da0b97e 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.5.1", + "version": "3.5.2", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", diff --git a/packages/compiler-dom/src/transforms/vOn.ts b/packages/compiler-dom/src/transforms/vOn.ts index 335b84a2ed1..618d12f7153 100644 --- a/packages/compiler-dom/src/transforms/vOn.ts +++ b/packages/compiler-dom/src/transforms/vOn.ts @@ -35,7 +35,7 @@ const isKeyboardEvent = /*@__PURE__*/ makeMap( const resolveModifiers = ( key: ExpressionNode, - modifiers: string[], + modifiers: SimpleExpressionNode[], context: TransformContext, loc: SourceLocation, ) => { @@ -44,7 +44,7 @@ const resolveModifiers = ( const eventOptionModifiers = [] for (let i = 0; i < modifiers.length; i++) { - const modifier = modifiers[i] + const modifier = modifiers[i].content if ( __COMPAT__ && diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 41fbfdcf1da..fd417236743 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.5.1", + "version": "3.5.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 29b7c49f14b..e40bb080534 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.5.1", + "version": "3.5.2", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index 08b034f79f1..e0b47cf56eb 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -1,4 +1,6 @@ import { + type TestElement, + defineComponent, h, nextTick, nodeOps, @@ -6,6 +8,7 @@ import { onUnmounted, render, serializeInner, + triggerEvent, } from '@vue/runtime-test' import { type DebuggerEvent, @@ -33,6 +36,20 @@ describe('reactivity/computed', () => { expect(cValue.value).toBe(1) }) + it('pass oldValue to computed getter', () => { + const count = ref(0) + const oldValue = ref() + const curValue = computed(pre => { + oldValue.value = pre + return count.value + }) + expect(curValue.value).toBe(0) + expect(oldValue.value).toBe(undefined) + count.value++ + expect(curValue.value).toBe(1) + expect(oldValue.value).toBe(0) + }) + it('should compute lazily', () => { const value = reactive<{ foo?: number }>({}) const getter = vi.fn(() => value.foo) @@ -577,7 +594,7 @@ describe('reactivity/computed', () => { v.value += ' World' await nextTick() - expect(serializeInner(root)).toBe('Hello World World World') + expect(serializeInner(root)).toBe('Hello World World World World') // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() }) @@ -875,7 +892,7 @@ describe('reactivity/computed', () => { v.value += ' World' await nextTick() expect(serializeInner(root)).toBe( - 'Hello World World World | Hello World World World', + 'Hello World World World World | Hello World World World World', ) }) @@ -944,4 +961,47 @@ describe('reactivity/computed', () => { newValue: 2, }) }) + + // #11797 + test('should prevent endless recursion in self-referencing computed getters', async () => { + const Comp = defineComponent({ + data() { + return { + counter: 0, + } + }, + + computed: { + message(): string { + if (this.counter === 0) { + this.counter++ + return this.message + } else { + return `Step ${this.counter}` + } + }, + }, + + render() { + return [ + h( + 'button', + { + onClick: () => { + this.counter++ + }, + }, + 'Step', + ), + h('p', this.message), + ] + }, + }) + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`

Step 1

`) + triggerEvent(root.children[1] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe(`

Step 2

`) + }) }) diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index a9586b34304..08d3443cefd 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.5.1", + "version": "3.5.2", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index aa5d2079061..d2dd67bf97c 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -111,9 +111,9 @@ export class ComputedRefImpl implements Subscriber { * @internal */ notify(): void { + this.flags |= EffectFlags.DIRTY // avoid infinite self recursion if (activeSub !== this) { - this.flags |= EffectFlags.DIRTY this.dep.notify() } else if (__DEV__) { // TODO warn diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index 4ce73ac9954..6d938cbc25f 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -46,7 +46,7 @@ export class Dep { } track(debugInfo?: DebuggerEventExtraInfo): Link | undefined { - if (!activeSub || !shouldTrack) { + if (!activeSub || !shouldTrack || activeSub === this.computed) { return } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index a22990f6729..51df32e9969 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -345,9 +345,6 @@ function isDirty(sub: Subscriber): boolean { * @internal */ export function refreshComputed(computed: ComputedRefImpl): false | undefined { - if (computed.flags & EffectFlags.RUNNING) { - return false - } if ( computed.flags & EffectFlags.TRACKING && !(computed.flags & EffectFlags.DIRTY) @@ -381,7 +378,7 @@ export function refreshComputed(computed: ComputedRefImpl): false | undefined { try { prepareDeps(computed) - const value = computed.fn() + const value = computed.fn(computed._value) if (dep.version === 0 || hasChanged(value, computed._value)) { computed._value = value dep.version++ diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index c3104c055f6..a72e52090dd 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -243,7 +243,10 @@ export function toValue(source: MaybeRefOrGetter): T { } const shallowUnwrapHandlers: ProxyHandler = { - get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)), + get: (target, key, receiver) => + key === ReactiveFlags.RAW + ? target + : unref(Reflect.get(target, key, receiver)), set: (target, key, value, receiver) => { const oldValue = target[key] if (isRef(oldValue) && !isRef(value)) { diff --git a/packages/runtime-core/__tests__/helpers/useTemplateRef.spec.ts b/packages/runtime-core/__tests__/helpers/useTemplateRef.spec.ts index 10de6f48353..adc8ed66c77 100644 --- a/packages/runtime-core/__tests__/helpers/useTemplateRef.spec.ts +++ b/packages/runtime-core/__tests__/helpers/useTemplateRef.spec.ts @@ -1,4 +1,5 @@ import { + type ShallowRef, h, nextTick, nodeOps, @@ -84,12 +85,12 @@ describe('useTemplateRef', () => { }) // #11795 - test('should work when variable name is same as key', () => { - let tRef + test('should not attempt to set when variable name is same as key', () => { + let tRef: ShallowRef const key = 'refKey' const Comp = { setup() { - tRef = useTemplateRef(key) + tRef = useTemplateRef('_') return { [key]: tRef, } @@ -102,5 +103,26 @@ describe('useTemplateRef', () => { render(h(Comp), root) expect('target is readonly').not.toHaveBeenWarned() + expect(tRef!.value).toBe(null) + }) + + test('should work when used as direct ref value (compiled in prod mode)', () => { + __DEV__ = false + try { + let foo: ShallowRef + const Comp = { + setup() { + foo = useTemplateRef('foo') + return () => h('div', { ref: foo }) + }, + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + + expect('target is readonly').not.toHaveBeenWarned() + expect(foo!.value).toBe(root.children[0]) + } finally { + __DEV__ = true + } }) }) diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index 195c10cb3e4..79f2104f255 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.5.1", + "version": "3.5.2", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 0a58954526c..138f185fca2 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -68,6 +68,7 @@ export type DefineComponent< Provide extends ComponentProvideOptions = ComponentProvideOptions, MakeDefaultsOptional extends boolean = true, TypeRefs extends Record = {}, + TypeEl extends Element = any, > = ComponentPublicInstanceConstructor< CreateComponentPublicInstanceWithMixins< Props, @@ -86,7 +87,8 @@ export type DefineComponent< LC & GlobalComponents, Directives & GlobalDirectives, Exposed, - TypeRefs + TypeRefs, + TypeEl > > & ComponentOptionsBase< @@ -134,6 +136,9 @@ export type DefineSetupFnComponent< S > +type ToResolvedProps = Readonly & + Readonly> + // defineComponent is a utility that is primarily used for type inference // when declaring components. Type inference is provided in the component // options (provided as the argument). The returned value has artificial types @@ -210,9 +215,8 @@ export function defineComponent< : ExtractPropTypes : { [key in RuntimePropsKeys]?: any } : TypeProps, - ResolvedProps = Readonly & - Readonly>, TypeRefs extends Record = {}, + TypeEl extends Element = any, >( options: { props?: (RuntimePropsOptions & ThisType) | RuntimePropsKeys[] @@ -228,8 +232,12 @@ export function defineComponent< * @private for language-tools use only */ __typeRefs?: TypeRefs + /** + * @private for language-tools use only + */ + __typeEl?: TypeEl } & ComponentOptionsBase< - ResolvedProps, + ToResolvedProps, SetupBindings, Data, Computed, @@ -249,7 +257,7 @@ export function defineComponent< > & ThisType< CreateComponentPublicInstanceWithMixins< - ResolvedProps, + ToResolvedProps, SetupBindings, Data, Computed, @@ -278,7 +286,7 @@ export function defineComponent< ResolvedEmits, RuntimeEmitsKeys, PublicProps, - ResolvedProps, + ToResolvedProps, ExtractDefaultPropTypes, Slots, LocalComponents, @@ -288,7 +296,8 @@ export function defineComponent< // MakeDefaultsOptional - if TypeProps is provided, set to false to use // user props types verbatim unknown extends TypeProps ? true : false, - TypeRefs + TypeRefs, + TypeEl > // implementation, close to no-op diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 5ba7c34cdfc..e9e7770ebd9 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -233,6 +233,7 @@ export type CreateComponentPublicInstanceWithMixins< Directives extends Record = {}, Exposed extends string = string, TypeRefs extends Data = {}, + TypeEl extends Element = any, Provide extends ComponentProvideOptions = ComponentProvideOptions, // mixin inference PublicMixin = IntersectionMixin & IntersectionMixin, @@ -277,7 +278,8 @@ export type CreateComponentPublicInstanceWithMixins< I, S, Exposed, - TypeRefs + TypeRefs, + TypeEl > export type ExposedKeys< @@ -302,6 +304,7 @@ export type ComponentPublicInstance< S extends SlotsType = {}, Exposed extends string = '', TypeRefs extends Data = {}, + TypeEl extends Element = any, > = { $: ComponentInternalInstance $data: D @@ -315,7 +318,7 @@ export type ComponentPublicInstance< $parent: ComponentPublicInstance | null $host: Element | null $emit: EmitFn - $el: any + $el: TypeEl $options: Options & MergedComponentOptionsOverride $forceUpdate: () => void $nextTick: typeof nextTick diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 5badb04b006..9fe381ff645 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -60,6 +60,7 @@ export function renderComponentRoot( setupState, ctx, inheritAttrs, + isMounted, } = instance const prev = setCurrentRenderingInstance(instance) @@ -253,7 +254,9 @@ export function renderComponentRoot( `that cannot be animated.`, ) } - root.transition = vnode.transition + root.transition = isMounted + ? vnode.component!.subTree.transition! + : vnode.transition } if (__DEV__ && setRoot) { diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 37534ad699f..568a6382bfe 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -227,6 +227,7 @@ const BaseTransitionImpl: ComponentOptions = { if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) { instance.update() } + delete leavingHooks.afterLeave } return emptyPlaceholder(child) } else if (mode === 'in-out' && innerChild.type !== Comment) { diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index a87f44cc8fa..dd1d1f5a6e3 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -267,7 +267,7 @@ const KeepAliveImpl: ComponentOptions = { pendingCacheKey = null if (!slots.default) { - return null + return (current = null) } const children = slots.default() diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index c7562436179..85001f500cf 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -566,7 +566,7 @@ function createSuspenseBoundary( // (got `pendingBranch.el`). // Therefore, after the mounting of activeBranch is completed, // it is necessary to get the latest anchor. - if (parentNode(activeBranch.el!) !== suspense.hiddenContainer) { + if (parentNode(activeBranch.el!) === container) { anchor = next(activeBranch) } unmount(activeBranch, parentComponent, suspense, true) diff --git a/packages/runtime-core/src/helpers/useTemplateRef.ts b/packages/runtime-core/src/helpers/useTemplateRef.ts index 58c109a9246..4cb10ea8139 100644 --- a/packages/runtime-core/src/helpers/useTemplateRef.ts +++ b/packages/runtime-core/src/helpers/useTemplateRef.ts @@ -3,6 +3,8 @@ import { getCurrentInstance } from '../component' import { warn } from '../warning' import { EMPTY_OBJ } from '@vue/shared' +export const knownTemplateRefs: WeakSet = new WeakSet() + export function useTemplateRef( key: Keys, ): Readonly> { @@ -10,7 +12,6 @@ export function useTemplateRef( const r = shallowRef(null) if (i) { const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs - let desc: PropertyDescriptor | undefined if ( __DEV__ && @@ -31,5 +32,9 @@ export function useTemplateRef( `instance to be associated with.`, ) } - return (__DEV__ ? readonly(r) : r) as any + const ret = __DEV__ ? readonly(r) : r + if (__DEV__) { + knownTemplateRefs.add(ret) + } + return ret } diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts index c7b15fe4022..1ffe3035794 100644 --- a/packages/runtime-core/src/rendererTemplateRef.ts +++ b/packages/runtime-core/src/rendererTemplateRef.ts @@ -11,11 +11,12 @@ import { } from '@vue/shared' import { isAsyncWrapper } from './apiAsyncComponent' import { warn } from './warning' -import { isRef } from '@vue/reactivity' +import { isRef, toRaw } from '@vue/reactivity' import { ErrorCodes, callWithErrorHandling } from './errorHandling' import type { SchedulerJob } from './scheduler' import { queuePostRenderEffect } from './renderer' import { getComponentPublicInstance } from './component' +import { knownTemplateRefs } from './helpers/useTemplateRef' /** * Function for handling a template ref @@ -63,12 +64,16 @@ export function setRef( const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs const setupState = owner.setupState + const rawSetupState = toRaw(setupState) const canSetSetupRef = setupState === EMPTY_OBJ ? () => false - : (key: string) => - hasOwn(setupState, key) && - !(Object.getOwnPropertyDescriptor(refs, key) || EMPTY_OBJ).get + : (key: string) => { + if (__DEV__ && knownTemplateRefs.has(rawSetupState[key] as any)) { + return false + } + return hasOwn(rawSetupState, key) + } // dynamic ref changed. unset old ref if (oldRef != null && oldRef !== ref) { diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 7f52a77bd50..2250af41110 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -1,5 +1,5 @@ import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling' -import { type Awaited, NOOP, isArray } from '@vue/shared' +import { NOOP, isArray } from '@vue/shared' import { type ComponentInternalInstance, getComponentName } from './component' export enum SchedulerJobFlags { @@ -108,9 +108,8 @@ export function queueJob(job: SchedulerJob): void { queue.splice(findInsertionIndex(jobId), 0, job) } - if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { - job.flags! |= SchedulerJobFlags.QUEUED - } + job.flags! |= SchedulerJobFlags.QUEUED + queueFlush() } } @@ -128,9 +127,7 @@ export function queuePostFlushCb(cb: SchedulerJobs): void { activePostFlushCbs.splice(postFlushIndex + 1, 0, cb) } else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) { pendingPostFlushCbs.push(cb) - if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { - cb.flags! |= SchedulerJobFlags.QUEUED - } + cb.flags! |= SchedulerJobFlags.QUEUED } } else { // if cb is an array, it is a component lifecycle hook which can only be @@ -161,6 +158,9 @@ export function flushPreFlushCbs( } queue.splice(i, 1) i-- + if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) { + cb.flags! &= ~SchedulerJobFlags.QUEUED + } cb() cb.flags! &= ~SchedulerJobFlags.QUEUED } @@ -194,6 +194,9 @@ export function flushPostFlushCbs(seen?: CountMap): void { if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { continue } + if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) { + cb.flags! &= ~SchedulerJobFlags.QUEUED + } if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) cb() cb.flags! &= ~SchedulerJobFlags.QUEUED } @@ -228,6 +231,9 @@ function flushJobs(seen?: CountMap) { if (__DEV__ && check(job)) { continue } + if (job.flags! & SchedulerJobFlags.ALLOW_RECURSE) { + job.flags! &= ~SchedulerJobFlags.QUEUED + } callWithErrorHandling( job, job.i, @@ -237,6 +243,14 @@ function flushJobs(seen?: CountMap) { } } } finally { + // If there was an error we still need to clear the QUEUED flags + for (; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex] + if (job) { + job.flags! &= ~SchedulerJobFlags.QUEUED + } + } + flushIndex = 0 queue.length = 0 diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 3ecb3b683a9..6af3be93a10 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.5.1", + "version": "3.5.2", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", diff --git a/packages/runtime-dom/src/jsx.ts b/packages/runtime-dom/src/jsx.ts index 4b67f16a97e..5292441cde9 100644 --- a/packages/runtime-dom/src/jsx.ts +++ b/packages/runtime-dom/src/jsx.ts @@ -405,6 +405,7 @@ export interface DataHTMLAttributes extends HTMLAttributes { } export interface DetailsHTMLAttributes extends HTMLAttributes { + name?: string open?: Booleanish onToggle?: (payload: ToggleEvent) => void } @@ -416,6 +417,7 @@ export interface DelHTMLAttributes extends HTMLAttributes { export interface DialogHTMLAttributes extends HTMLAttributes { open?: Booleanish + onClose?: (payload: Event) => void } export interface EmbedHTMLAttributes extends HTMLAttributes { diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index 4dd6718c66d..078493d7689 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.5.1", + "version": "3.5.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 753dbe9322e..3de674cb693 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.5.1", + "version": "3.5.2", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", diff --git a/packages/shared/src/typeUtils.ts b/packages/shared/src/typeUtils.ts index 4846751b84e..1fd161ceb05 100644 --- a/packages/shared/src/typeUtils.ts +++ b/packages/shared/src/typeUtils.ts @@ -13,15 +13,6 @@ export type LooseRequired = { [P in keyof (T & Required)]: T[P] } // https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 export type IfAny = 0 extends 1 & T ? Y : N -// To prevent users with TypeScript versions lower than 4.5 from encountering unsupported Awaited type, a copy has been made here. -export type Awaited = T extends null | undefined - ? T // special case for `null | undefined` when not in `--strictNullChecks` mode - : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped - ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument - ? Awaited // recursively unwrap the value - : never // the argument to `then` was not callable - : T // non-object or non-thenable - /** * Utility for extracting the parameters from a function overload (for typed emits) * https://github.com/microsoft/TypeScript/issues/32164#issuecomment-1146737709 diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index fa24c0e41a6..1f0ebbf6c18 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compat", - "version": "3.5.1", + "version": "3.5.2", "description": "Vue 3 compatibility build for Vue 2", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index b9e9117289e..c0863a75991 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1427,9 +1427,11 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + }) + describe('transition with KeepAlive', () => { test( - 'w/ KeepAlive + unmount innerChild', + 'unmount innerChild (out-in mode)', async () => { const unmountSpy = vi.fn() await page().exposeFunction('unmountSpy', unmountSpy) @@ -1484,6 +1486,173 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + + // #11775 + test( + 'switch child then update include (out-in mode)', + async () => { + const onUpdatedSpyA = vi.fn() + const onUnmountedSpyC = vi.fn() + + await page().exposeFunction('onUpdatedSpyA', onUpdatedSpyA) + await page().exposeFunction('onUnmountedSpyC', onUnmountedSpyC) + + await page().evaluate(() => { + const { onUpdatedSpyA, onUnmountedSpyC } = window as any + const { createApp, ref, shallowRef, h, onUpdated, onUnmounted } = ( + window as any + ).Vue + createApp({ + template: ` +
+ + + + + +
+ + + + `, + components: { + CompA: { + name: 'CompA', + setup() { + onUpdated(onUpdatedSpyA) + return () => h('div', 'CompA') + }, + }, + CompB: { + name: 'CompB', + setup() { + return () => h('div', 'CompB') + }, + }, + CompC: { + name: 'CompC', + setup() { + onUnmounted(onUnmountedSpyC) + return () => h('div', 'CompC') + }, + }, + }, + setup: () => { + const includeRef = ref(['CompA', 'CompB', 'CompC']) + const current = shallowRef('CompA') + const switchToB = () => (current.value = 'CompB') + const switchToC = () => (current.value = 'CompC') + const switchToA = () => { + current.value = 'CompA' + includeRef.value = ['CompA'] + } + return { current, switchToB, switchToC, switchToA, includeRef } + }, + }).mount('#app') + }) + + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + await click('#switchToB') + await nextTick() + await click('#switchToC') + await transitionFinish() + expect(await html('#container')).toBe('
CompC
') + + await click('#switchToA') + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + // expect CompA only update once + expect(onUpdatedSpyA).toBeCalledTimes(1) + expect(onUnmountedSpyC).toBeCalledTimes(1) + }, + E2E_TIMEOUT, + ) + + // #10827 + test( + 'switch and update child then update include (out-in mode)', + async () => { + const onUnmountedSpyB = vi.fn() + await page().exposeFunction('onUnmountedSpyB', onUnmountedSpyB) + + await page().evaluate(() => { + const { onUnmountedSpyB } = window as any + const { + createApp, + ref, + shallowRef, + h, + provide, + inject, + onUnmounted, + } = (window as any).Vue + createApp({ + template: ` +
+ + + + + +
+ + + `, + components: { + CompA: { + name: 'CompA', + setup() { + const current = inject('current') + return () => h('div', current.value) + }, + }, + CompB: { + name: 'CompB', + setup() { + const current = inject('current') + onUnmounted(onUnmountedSpyB) + return () => h('div', current.value) + }, + }, + }, + setup: () => { + const includeRef = ref(['CompA']) + const current = shallowRef('CompA') + provide('current', current) + + const switchToB = () => { + current.value = 'CompB' + includeRef.value = ['CompA', 'CompB'] + } + const switchToA = () => { + current.value = 'CompA' + includeRef.value = ['CompA'] + } + return { current, switchToB, switchToA, includeRef } + }, + }).mount('#app') + }) + + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + await click('#switchToB') + await transitionFinish() + await transitionFinish() + expect(await html('#container')).toBe('
CompB
') + + await click('#switchToA') + await transitionFinish() + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + expect(onUnmountedSpyB).toBeCalledTimes(1) + }, + E2E_TIMEOUT, + ) }) describe('transition with Suspense', () => { @@ -1993,6 +2162,66 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + + // #11806 + test( + 'switch between Async and Sync child when transition is not finished', + async () => { + await page().evaluate(() => { + const { createApp, shallowRef, h, nextTick } = (window as any).Vue + createApp({ + template: ` +
+ + + + + +
+ + `, + setup: () => { + const view = shallowRef('SyncB') + const click = async () => { + view.value = 'SyncA' + await nextTick() + view.value = 'AsyncB' + await nextTick() + view.value = 'SyncB' + } + return { view, click } + }, + components: { + SyncA: { + setup() { + return () => h('div', 'SyncA') + }, + }, + AsyncB: { + async setup() { + await nextTick() + return () => h('div', 'AsyncB') + }, + }, + SyncB: { + setup() { + return () => h('div', 'SyncB') + }, + }, + }, + }).mount('#app') + }) + + expect(await html('#container')).toBe('
SyncB
') + + await click('#toggleBtn') + await nextFrame() + await transitionFinish() + await transitionFinish() + expect(await html('#container')).toBe('
SyncB
') + }, + E2E_TIMEOUT, + ) }) describe('transition with Teleport', () => { diff --git a/packages/vue/package.json b/packages/vue/package.json index c74dfc61f11..96541cacfac 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "3.5.1", + "version": "3.5.2", "description": "The progressive JavaScript framework for building modern web UI.", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js",