diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 58e69b1cc..2b7785ae7 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -7,6 +7,7 @@ import { } from './reactiveEffect' import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared' +import { warn } from './warning' type CollectionTypes = IterableCollections | WeakCollections @@ -223,7 +224,7 @@ function createReadonlyMethod(type: TriggerOpTypes): Function { return function (this: CollectionTypes, ...args: unknown[]) { if (__DEV__) { const key = args[0] ? `on key "${args[0]}" ` : `` - console.warn( + warn( `${capitalize(type)} operation ${key}failed: target is readonly.`, toRaw(this), ) @@ -397,7 +398,7 @@ function checkIdentityKeys( const rawKey = toRaw(key) if (rawKey !== key && has.call(target, rawKey)) { const type = toRawType(target) - console.warn( + warn( `Reactive ${type} contains both the raw and reactive ` + `versions of the same object${type === `Map` ? ` as keys` : ``}, ` + `which can lead to inconsistencies. ` + diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index a4b74172f..da63fe847 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -42,8 +42,13 @@ export class ComputedRefImpl { public _cacheable: boolean + /** + * Dev only + */ + _warnRecursive?: boolean + constructor( - getter: ComputedGetter, + private getter: ComputedGetter, private readonly _setter: ComputedSetter, isReadonly: boolean, isSSR: boolean, @@ -74,7 +79,9 @@ export class ComputedRefImpl { } trackRefValue(self) if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) { - __DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN) + if (__DEV__ && (__TEST__ || this._warnRecursive)) { + warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter) + } triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect) } return self._value diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index d78570676..821f19d6a 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -43,6 +43,7 @@ export { type WritableComputedOptions, type ComputedGetter, type ComputedSetter, + type ComputedRefImpl, } from './computed' export { deferredComputed } from './deferredComputed' export { diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 8b94dd9a4..1e0f9365d 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -13,6 +13,7 @@ import { } from './collectionHandlers' import type { RawSymbol, Ref, UnwrapRefSimple } from './ref' import { ReactiveFlags } from './constants' +import { warn } from './warning' export interface Target { [ReactiveFlags.SKIP]?: boolean @@ -247,7 +248,7 @@ function createReactiveObject( ) { if (!isObject(target)) { if (__DEV__) { - console.warn(`value cannot be made reactive: ${String(target)}`) + warn(`value cannot be made reactive: ${String(target)}`) } return target } diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 1b9d60ef0..5f40fbb7c 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -25,6 +25,7 @@ import type { ShallowReactiveMarker } from './reactive' import { type Dep, createDep } from './dep' import { ComputedRefImpl } from './computed' import { getDepFromReactive } from './reactiveEffect' +import { warn } from './warning' declare const RefSymbol: unique symbol export declare const RawSymbol: unique symbol @@ -345,7 +346,7 @@ export type ToRefs = { */ export function toRefs(object: T): ToRefs { if (__DEV__ && !isProxy(object)) { - console.warn(`toRefs() expects a reactive object but received a plain one.`) + warn(`toRefs() expects a reactive object but received a plain one.`) } const ret: any = isArray(object) ? new Array(object.length) : {} for (const key in object) { diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 928da872f..fd1913b2c 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -22,7 +22,7 @@ import { watch, watchEffect, } from '@vue/runtime-test' -import { createApp, defineComponent } from 'vue' +import { computed, createApp, defineComponent, inject, provide } from 'vue' import type { RawSlots } from 'packages/runtime-core/src/componentSlots' import { resetSuspenseId } from '../../src/components/Suspense' @@ -1039,6 +1039,99 @@ describe('Suspense', () => { expect(serializeInner(root)).toBe(`
foo
foo nested
`) }) + // #10098 + test('switching branches w/ nested suspense', async () => { + const RouterView = { + setup(_: any, { slots }: any) { + const route = inject('route') as any + const depth = inject('depth', 0) + provide('depth', depth + 1) + return () => { + const current = route.value[depth] + return slots.default({ Component: current })[0] + } + }, + } + + const OuterB = defineAsyncComponent({ + setup: () => { + return () => + h(RouterView, null, { + default: ({ Component }: any) => [ + h(Suspense, null, { + default: () => h(Component), + }), + ], + }) + }, + }) + + const InnerB = defineAsyncComponent({ + setup: () => { + return () => h('div', 'innerB') + }, + }) + + const OuterA = defineAsyncComponent({ + setup: () => { + return () => + h(RouterView, null, { + default: ({ Component }: any) => [ + h(Suspense, null, { + default: () => h(Component), + }), + ], + }) + }, + }) + + const InnerA = defineAsyncComponent({ + setup: () => { + return () => h('div', 'innerA') + }, + }) + + const toggle = ref(true) + const route = computed(() => { + return toggle.value ? [OuterA, InnerA] : [OuterB, InnerB] + }) + + const Comp = { + setup() { + provide('route', route) + return () => + h(RouterView, null, { + default: ({ Component }: any) => [ + h(Suspense, null, { + default: () => h(Component), + }), + ], + }) + }, + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(``) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
innerA
`) + + deps.length = 0 + + toggle.value = false + await nextTick() + // toggle again + toggle.value = true + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
innerA
`) + }) + test('branch switch to 3rd branch before resolve', async () => { const calls: string[] = [] diff --git a/packages/runtime-core/src/apiComputed.ts b/packages/runtime-core/src/apiComputed.ts index 97db0da45..a7d959dfa 100644 --- a/packages/runtime-core/src/apiComputed.ts +++ b/packages/runtime-core/src/apiComputed.ts @@ -1,10 +1,17 @@ -import { computed as _computed } from '@vue/reactivity' -import { isInSSRComponentSetup } from './component' +import { type ComputedRefImpl, computed as _computed } from '@vue/reactivity' +import { getCurrentInstance, isInSSRComponentSetup } from './component' export const computed: typeof _computed = ( getterOrOptions: any, debugOptions?: any, ) => { // @ts-expect-error - return _computed(getterOrOptions, debugOptions, isInSSRComponentSetup) + const c = _computed(getterOrOptions, debugOptions, isInSSRComponentSetup) + if (__DEV__) { + const i = getCurrentInstance() + if (i && i.appContext.config.warnRecursiveComputed) { + ;(c as unknown as ComputedRefImpl)._warnRecursive = true + } + } + return c } diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index f4754fdb7..1678c3e34 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -83,7 +83,7 @@ export type OptionMergeFunction = (to: unknown, from: unknown) => any export interface AppConfig { // @private - readonly isNativeTag?: (tag: string) => boolean + readonly isNativeTag: (tag: string) => boolean performance: boolean optionMergeStrategies: Record @@ -109,6 +109,12 @@ export interface AppConfig { * @deprecated use config.compilerOptions.isCustomElement */ isCustomElement?: (tag: string) => boolean + + /** + * TODO document for 3.5 + * Enable warnings for computed getters that recursively trigger itself. + */ + warnRecursiveComputed?: boolean } export interface AppContext { diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 96a98b336..1a0f315c5 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -62,7 +62,6 @@ import { type Data, EMPTY_OBJ, type IfAny, - NO, NOOP, ShapeFlags, extend, @@ -706,9 +705,11 @@ export const unsetCurrentInstance = () => { const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component') -export function validateComponentName(name: string, config: AppConfig) { - const appIsNativeTag = config.isNativeTag || NO - if (isBuiltInTag(name) || appIsNativeTag(name)) { +export function validateComponentName( + name: string, + { isNativeTag }: AppConfig, +) { + if (isBuiltInTag(name) || isNativeTag(name)) { warn( 'Do not use built-in or reserved HTML elements as component id: ' + name, ) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 9b3d6765d..65b05c3dd 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -100,7 +100,9 @@ export const SuspenseImpl = { // it is necessary to skip the current patch to avoid multiple mounts // of inner components. if (parentSuspense && parentSuspense.deps > 0) { - n2.suspense = n1.suspense + n2.suspense = n1.suspense! + n2.suspense.vnode = n2 + n2.el = n1.el return } patchSuspense( diff --git a/packages/runtime-dom/__tests__/patchStyle.spec.ts b/packages/runtime-dom/__tests__/patchStyle.spec.ts index e7cd0984a..8b2765e21 100644 --- a/packages/runtime-dom/__tests__/patchStyle.spec.ts +++ b/packages/runtime-dom/__tests__/patchStyle.spec.ts @@ -158,4 +158,13 @@ describe(`runtime-dom: style patching`, () => { ) expect(el.style.display).toBe('flex') }) + + it('should clear previous css string value', () => { + const el = document.createElement('div') + patchProp(el, 'style', {}, 'color:red') + expect(el.style.cssText.replace(/\s/g, '')).toBe('color:red;') + + patchProp(el, 'style', 'color:red', { fontSize: '12px' }) + expect(el.style.cssText.replace(/\s/g, '')).toBe('font-size:12px;') + }) }) diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index b2450b3cf..9e94810d8 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -209,25 +209,20 @@ export const vModelSelect: ModelDirective = { }, // set value in mounted & updated because