Merge remote-tracking branch 'upstream/main'

This commit is contained in:
三咲智子 Kevin Deng 2024-02-25 21:07:09 +08:00
commit 5819dc9001
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
17 changed files with 178 additions and 46 deletions

View File

@ -7,6 +7,7 @@ import {
} from './reactiveEffect' } from './reactiveEffect'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared' import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'
import { warn } from './warning'
type CollectionTypes = IterableCollections | WeakCollections type CollectionTypes = IterableCollections | WeakCollections
@ -223,7 +224,7 @@ function createReadonlyMethod(type: TriggerOpTypes): Function {
return function (this: CollectionTypes, ...args: unknown[]) { return function (this: CollectionTypes, ...args: unknown[]) {
if (__DEV__) { if (__DEV__) {
const key = args[0] ? `on key "${args[0]}" ` : `` const key = args[0] ? `on key "${args[0]}" ` : ``
console.warn( warn(
`${capitalize(type)} operation ${key}failed: target is readonly.`, `${capitalize(type)} operation ${key}failed: target is readonly.`,
toRaw(this), toRaw(this),
) )
@ -397,7 +398,7 @@ function checkIdentityKeys(
const rawKey = toRaw(key) const rawKey = toRaw(key)
if (rawKey !== key && has.call(target, rawKey)) { if (rawKey !== key && has.call(target, rawKey)) {
const type = toRawType(target) const type = toRawType(target)
console.warn( warn(
`Reactive ${type} contains both the raw and reactive ` + `Reactive ${type} contains both the raw and reactive ` +
`versions of the same object${type === `Map` ? ` as keys` : ``}, ` + `versions of the same object${type === `Map` ? ` as keys` : ``}, ` +
`which can lead to inconsistencies. ` + `which can lead to inconsistencies. ` +

View File

@ -42,8 +42,13 @@ export class ComputedRefImpl<T> {
public _cacheable: boolean public _cacheable: boolean
/**
* Dev only
*/
_warnRecursive?: boolean
constructor( constructor(
getter: ComputedGetter<T>, private getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>, private readonly _setter: ComputedSetter<T>,
isReadonly: boolean, isReadonly: boolean,
isSSR: boolean, isSSR: boolean,
@ -74,7 +79,9 @@ export class ComputedRefImpl<T> {
} }
trackRefValue(self) trackRefValue(self)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) { 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) triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
} }
return self._value return self._value

View File

@ -43,6 +43,7 @@ export {
type WritableComputedOptions, type WritableComputedOptions,
type ComputedGetter, type ComputedGetter,
type ComputedSetter, type ComputedSetter,
type ComputedRefImpl,
} from './computed' } from './computed'
export { deferredComputed } from './deferredComputed' export { deferredComputed } from './deferredComputed'
export { export {

View File

@ -13,6 +13,7 @@ import {
} from './collectionHandlers' } from './collectionHandlers'
import type { RawSymbol, Ref, UnwrapRefSimple } from './ref' import type { RawSymbol, Ref, UnwrapRefSimple } from './ref'
import { ReactiveFlags } from './constants' import { ReactiveFlags } from './constants'
import { warn } from './warning'
export interface Target { export interface Target {
[ReactiveFlags.SKIP]?: boolean [ReactiveFlags.SKIP]?: boolean
@ -247,7 +248,7 @@ function createReactiveObject(
) { ) {
if (!isObject(target)) { if (!isObject(target)) {
if (__DEV__) { if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`) warn(`value cannot be made reactive: ${String(target)}`)
} }
return target return target
} }

View File

@ -25,6 +25,7 @@ import type { ShallowReactiveMarker } from './reactive'
import { type Dep, createDep } from './dep' import { type Dep, createDep } from './dep'
import { ComputedRefImpl } from './computed' import { ComputedRefImpl } from './computed'
import { getDepFromReactive } from './reactiveEffect' import { getDepFromReactive } from './reactiveEffect'
import { warn } from './warning'
declare const RefSymbol: unique symbol declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol export declare const RawSymbol: unique symbol
@ -345,7 +346,7 @@ export type ToRefs<T = any> = {
*/ */
export function toRefs<T extends object>(object: T): ToRefs<T> { export function toRefs<T extends object>(object: T): ToRefs<T> {
if (__DEV__ && !isProxy(object)) { 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) : {} const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) { for (const key in object) {

View File

@ -22,7 +22,7 @@ import {
watch, watch,
watchEffect, watchEffect,
} from '@vue/runtime-test' } 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 type { RawSlots } from 'packages/runtime-core/src/componentSlots'
import { resetSuspenseId } from '../../src/components/Suspense' import { resetSuspenseId } from '../../src/components/Suspense'
@ -1039,6 +1039,99 @@ describe('Suspense', () => {
expect(serializeInner(root)).toBe(`<div>foo<div>foo nested</div></div>`) expect(serializeInner(root)).toBe(`<div>foo<div>foo nested</div></div>`)
}) })
// #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(`<div>innerA</div>`)
deps.length = 0
toggle.value = false
await nextTick()
// toggle again
toggle.value = true
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>innerA</div>`)
})
test('branch switch to 3rd branch before resolve', async () => { test('branch switch to 3rd branch before resolve', async () => {
const calls: string[] = [] const calls: string[] = []

View File

@ -1,10 +1,17 @@
import { computed as _computed } from '@vue/reactivity' import { type ComputedRefImpl, computed as _computed } from '@vue/reactivity'
import { isInSSRComponentSetup } from './component' import { getCurrentInstance, isInSSRComponentSetup } from './component'
export const computed: typeof _computed = ( export const computed: typeof _computed = (
getterOrOptions: any, getterOrOptions: any,
debugOptions?: any, debugOptions?: any,
) => { ) => {
// @ts-expect-error // @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<any>)._warnRecursive = true
}
}
return c
} }

View File

@ -83,7 +83,7 @@ export type OptionMergeFunction = (to: unknown, from: unknown) => any
export interface AppConfig { export interface AppConfig {
// @private // @private
readonly isNativeTag?: (tag: string) => boolean readonly isNativeTag: (tag: string) => boolean
performance: boolean performance: boolean
optionMergeStrategies: Record<string, OptionMergeFunction> optionMergeStrategies: Record<string, OptionMergeFunction>
@ -109,6 +109,12 @@ export interface AppConfig {
* @deprecated use config.compilerOptions.isCustomElement * @deprecated use config.compilerOptions.isCustomElement
*/ */
isCustomElement?: (tag: string) => boolean isCustomElement?: (tag: string) => boolean
/**
* TODO document for 3.5
* Enable warnings for computed getters that recursively trigger itself.
*/
warnRecursiveComputed?: boolean
} }
export interface AppContext { export interface AppContext {

View File

@ -62,7 +62,6 @@ import {
type Data, type Data,
EMPTY_OBJ, EMPTY_OBJ,
type IfAny, type IfAny,
NO,
NOOP, NOOP,
ShapeFlags, ShapeFlags,
extend, extend,
@ -706,9 +705,11 @@ export const unsetCurrentInstance = () => {
const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component') const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
export function validateComponentName(name: string, config: AppConfig) { export function validateComponentName(
const appIsNativeTag = config.isNativeTag || NO name: string,
if (isBuiltInTag(name) || appIsNativeTag(name)) { { isNativeTag }: AppConfig,
) {
if (isBuiltInTag(name) || isNativeTag(name)) {
warn( warn(
'Do not use built-in or reserved HTML elements as component id: ' + name, 'Do not use built-in or reserved HTML elements as component id: ' + name,
) )

View File

@ -100,7 +100,9 @@ export const SuspenseImpl = {
// it is necessary to skip the current patch to avoid multiple mounts // it is necessary to skip the current patch to avoid multiple mounts
// of inner components. // of inner components.
if (parentSuspense && parentSuspense.deps > 0) { if (parentSuspense && parentSuspense.deps > 0) {
n2.suspense = n1.suspense n2.suspense = n1.suspense!
n2.suspense.vnode = n2
n2.el = n1.el
return return
} }
patchSuspense( patchSuspense(

View File

@ -158,4 +158,13 @@ describe(`runtime-dom: style patching`, () => {
) )
expect(el.style.display).toBe('flex') 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;')
})
}) })

View File

@ -209,25 +209,20 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
}, },
// set value in mounted & updated because <select> relies on its children // set value in mounted & updated because <select> relies on its children
// <option>s. // <option>s.
mounted(el, { value, oldValue, modifiers: { number } }) { mounted(el, { value, modifiers: { number } }) {
setSelected(el, value, oldValue, number) setSelected(el, value, number)
}, },
beforeUpdate(el, _binding, vnode) { beforeUpdate(el, _binding, vnode) {
el[assignKey] = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
}, },
updated(el, { value, oldValue, modifiers: { number } }) { updated(el, { value, modifiers: { number } }) {
if (!el._assigning) { if (!el._assigning) {
setSelected(el, value, oldValue, number) setSelected(el, value, number)
} }
}, },
} }
function setSelected( function setSelected(el: HTMLSelectElement, value: any, number: boolean) {
el: HTMLSelectElement,
value: any,
oldValue: any,
number: boolean,
) {
const isMultiple = el.multiple const isMultiple = el.multiple
const isArrayValue = isArray(value) const isArrayValue = isArray(value)
if (isMultiple && !isArrayValue && !isSet(value)) { if (isMultiple && !isArrayValue && !isSet(value)) {
@ -256,11 +251,9 @@ function setSelected(
} else { } else {
option.selected = value.has(optionValue) option.selected = value.has(optionValue)
} }
} else { } else if (looseEqual(getValue(option), value)) {
if (looseEqual(getValue(option), value)) { if (el.selectedIndex !== i) el.selectedIndex = i
if (el.selectedIndex !== i) el.selectedIndex = i return
return
}
} }
} }
if (!isMultiple && el.selectedIndex !== -1) { if (!isMultiple && el.selectedIndex !== -1) {

View File

@ -39,7 +39,8 @@ export function patchDOMProp(
el._value = value el._value = value
// #4956: <option> value will fallback to its text content so we need to // #4956: <option> value will fallback to its text content so we need to
// compare against its attribute value instead. // compare against its attribute value instead.
const oldValue = tag === 'OPTION' ? el.getAttribute('value') : el.value const oldValue =
tag === 'OPTION' ? el.getAttribute('value') || '' : el.value
const newValue = value == null ? '' : value const newValue = value == null ? '' : value
if (oldValue !== newValue) { if (oldValue !== newValue) {
el.value = newValue el.value = newValue

View File

@ -13,10 +13,19 @@ export function patchStyle(el: Element, prev: Style, next: Style) {
const currentDisplay = style.display const currentDisplay = style.display
let hasControlledDisplay = false let hasControlledDisplay = false
if (next && !isCssString) { if (next && !isCssString) {
if (prev && !isString(prev)) { if (prev) {
for (const key in prev) { if (!isString(prev)) {
if (next[key] == null) { for (const key in prev) {
setStyle(style, key, '') if (next[key] == null) {
setStyle(style, key, '')
}
}
} else {
for (const prevStyle of prev.split(';')) {
const key = prevStyle.slice(0, prevStyle.indexOf(':')).trim()
if (next[key] == null) {
setStyle(style, key, '')
}
} }
} }
} }

View File

@ -13,7 +13,7 @@
"vite": "^5.0.12" "vite": "^5.0.12"
}, },
"dependencies": { "dependencies": {
"@vue/repl": "^4.1.0", "@vue/repl": "^4.1.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"vue": "workspace:*" "vue": "workspace:*"

View File

@ -12,7 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@vue/compiler-vapor": "workspace:^", "@vue/compiler-vapor": "workspace:^",
"monaco-editor": "^0.45.0", "monaco-editor": "^0.46.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
} }

View File

@ -374,8 +374,8 @@ importers:
packages/sfc-playground: packages/sfc-playground:
dependencies: dependencies:
'@vue/repl': '@vue/repl':
specifier: ^4.1.0 specifier: ^4.1.1
version: 4.1.0 version: 4.1.1
file-saver: file-saver:
specifier: ^2.0.5 specifier: ^2.0.5
version: 2.0.5 version: 2.0.5
@ -401,8 +401,8 @@ importers:
specifier: workspace:^ specifier: workspace:^
version: link:../compiler-vapor version: link:../compiler-vapor
monaco-editor: monaco-editor:
specifier: ^0.45.0 specifier: ^0.46.0
version: 0.45.0 version: 0.46.0
source-map-js: source-map-js:
specifier: ^1.0.2 specifier: ^1.0.2
version: 1.0.2 version: 1.0.2
@ -1896,8 +1896,8 @@ packages:
engines: {node: '>= 0.12.0'} engines: {node: '>= 0.12.0'}
dev: true dev: true
/@vue/repl@4.1.0: /@vue/repl@4.1.1:
resolution: {integrity: sha512-4ZNEQWlLjl1Sq+WFiACm5siMdwUAmmqOES4XDgZRRFYeeW/BfabO9I6fpU+Y0zO9HFzKb8dwUUH0e0LK7mIYeg==} resolution: {integrity: sha512-gkbnU+rM01/ILdnDJbsWS8+PW6qMAzprBo/U2+7eVci0kx6VAR26fL/qrcEPwEYa6q0vzzptZ4il0SaUGGqZKw==}
dev: false dev: false
/@vueuse/core@10.7.2(vue@packages+vue): /@vueuse/core@10.7.2(vue@packages+vue):
@ -4415,8 +4415,8 @@ packages:
ufo: 1.3.1 ufo: 1.3.1
dev: true dev: true
/monaco-editor@0.45.0: /monaco-editor@0.46.0:
resolution: {integrity: sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==} resolution: {integrity: sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==}
dev: false dev: false
/mrmime@1.0.1: /mrmime@1.0.1: