Просмотр исходного кода

Merge remote-tracking branch 'upstream/main'

三咲智子 Kevin Deng 2 лет назад
Родитель
Сommit
5819dc9001

+ 3 - 2
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. ` +

+ 9 - 2
packages/reactivity/src/computed.ts

@@ -42,8 +42,13 @@ export class ComputedRefImpl<T> {
 
   public _cacheable: boolean
 
+  /**
+   * Dev only
+   */
+  _warnRecursive?: boolean
+
   constructor(
-    getter: ComputedGetter<T>,
+    private getter: ComputedGetter<T>,
     private readonly _setter: ComputedSetter<T>,
     isReadonly: boolean,
     isSSR: boolean,
@@ -74,7 +79,9 @@ export class ComputedRefImpl<T> {
     }
     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

+ 1 - 0
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 {

+ 2 - 1
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
   }

+ 2 - 1
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<T = any> = {
  */
 export function toRefs<T extends object>(object: T): ToRefs<T> {
   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) {

+ 94 - 1
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(`<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 () => {
     const calls: string[] = []
 

+ 10 - 3
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<any>)._warnRecursive = true
+    }
+  }
+  return c
 }

+ 7 - 1
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<string, OptionMergeFunction>
@@ -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 {

+ 5 - 4
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,
     )

+ 3 - 1
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(

+ 9 - 0
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;')
+  })
 })

+ 8 - 15
packages/runtime-dom/src/directives/vModel.ts

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

+ 2 - 1
packages/runtime-dom/src/modules/props.ts

@@ -39,7 +39,8 @@ export function patchDOMProp(
     el._value = value
     // #4956: <option> value will fallback to its text content so we need to
     // 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
     if (oldValue !== newValue) {
       el.value = newValue

+ 13 - 4
packages/runtime-dom/src/modules/style.ts

@@ -13,10 +13,19 @@ export function patchStyle(el: Element, prev: Style, next: Style) {
   const currentDisplay = style.display
   let hasControlledDisplay = false
   if (next && !isCssString) {
-    if (prev && !isString(prev)) {
-      for (const key in prev) {
-        if (next[key] == null) {
-          setStyle(style, key, '')
+    if (prev) {
+      if (!isString(prev)) {
+        for (const key in prev) {
+          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, '')
+          }
         }
       }
     }

+ 1 - 1
packages/sfc-playground/package.json

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

+ 1 - 1
packages/template-explorer/package.json

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

+ 8 - 8
pnpm-lock.yaml

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