三咲智子 Kevin Deng 1 год назад
Родитель
Сommit
f0405f1528

+ 16 - 0
CHANGELOG.md

@@ -1,3 +1,19 @@
+# [3.5.0-alpha.4](https://github.com/vuejs/core/compare/v3.4.34...v3.5.0-alpha.4) (2024-07-24)
+
+
+### Bug Fixes
+
+* **suspense/hydration:** fix hydration timing of async component inside suspense ([1b8e197](https://github.com/vuejs/core/commit/1b8e197a5b65d67a9703b8511786fb81df9aa7cc)), closes [#6638](https://github.com/vuejs/core/issues/6638)
+* **useId:** properly mark async boundary for already resolved async component ([cd28172](https://github.com/vuejs/core/commit/cd281725781ada2ab279e919031ae307e146a9d9))
+
+
+
+## [3.4.34](https://github.com/vuejs/core/compare/v3.4.33...v3.4.34) (2024-07-24)
+
+* **defineModel:** correct update with multiple changes in same tick ([#11430](https://github.com/vuejs/core/issues/11430)) ([a18f1ec](https://github.com/vuejs/core/commit/a18f1ecf05842337f1eb39a6871adb8cb4024093)), closes [#11429](https://github.com/vuejs/core/issues/11429)
+
+
+
 # [3.5.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.33...v3.5.0-alpha.3) (2024-07-19)
 
 

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "private": true,
-  "version": "3.0.0-vapor",
+  "version": "3.5.0-alpha.4",
   "packageManager": "pnpm@9.5.0",
   "type": "module",
   "scripts": {

+ 1 - 1
packages/compiler-core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-core",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "@vue/compiler-core",
   "main": "index.js",
   "module": "dist/compiler-core.esm-bundler.js",

+ 1 - 1
packages/compiler-dom/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-dom",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "@vue/compiler-dom",
   "main": "index.js",
   "module": "dist/compiler-dom.esm-bundler.js",

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

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-sfc",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "@vue/compiler-sfc",
   "main": "dist/compiler-sfc.cjs.js",
   "module": "dist/compiler-sfc.esm-browser.js",

+ 1 - 1
packages/compiler-ssr/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compiler-ssr",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "@vue/compiler-ssr",
   "main": "dist/compiler-ssr.cjs.js",
   "types": "dist/compiler-ssr.d.ts",

+ 1 - 1
packages/reactivity/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/reactivity",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "@vue/reactivity",
   "main": "index.js",
   "module": "dist/reactivity.esm-bundler.js",

+ 55 - 9
packages/runtime-core/__tests__/helpers/useId.spec.ts

@@ -12,10 +12,11 @@ import {
 } from 'vue'
 import { renderToString } from '@vue/server-renderer'
 
-type TestCaseFactory = () => [App, Promise<any>[]]
+type FactoryRes = [App, Promise<any>[]]
+type TestCaseFactory = () => FactoryRes | Promise<FactoryRes>
 
 async function runOnClient(factory: TestCaseFactory) {
-  const [app, deps] = factory()
+  const [app, deps] = await factory()
   const root = document.createElement('div')
   app.mount(root)
   await Promise.all(deps)
@@ -24,7 +25,7 @@ async function runOnClient(factory: TestCaseFactory) {
 }
 
 async function runOnServer(factory: TestCaseFactory) {
-  const [app, _] = factory()
+  const [app, _] = await factory()
   return (await renderToString(app))
     .replace(/<!--[\[\]]-->/g, '') // remove fragment wrappers
     .trim()
@@ -95,8 +96,8 @@ describe('useId', () => {
       'v:0-0 v:0-1 ' + // inside first async subtree
       'v:1-0 v:1-1' // inside second async subtree
     // assert different async resolution order does not affect id stable-ness
-    expect(await getOutput(() => factory(10, 20))).toBe(expected)
-    expect(await getOutput(() => factory(20, 10))).toBe(expected)
+    expect(await getOutput(() => factory(0, 16))).toBe(expected)
+    expect(await getOutput(() => factory(16, 0))).toBe(expected)
   })
 
   test('serverPrefetch', async () => {
@@ -140,8 +141,8 @@ describe('useId', () => {
       'v:0-0 v:0-1 ' + // inside first async subtree
       'v:1-0 v:1-1' // inside second async subtree
     // assert different async resolution order does not affect id stable-ness
-    expect(await getOutput(() => factory(10, 20))).toBe(expected)
-    expect(await getOutput(() => factory(20, 10))).toBe(expected)
+    expect(await getOutput(() => factory(0, 16))).toBe(expected)
+    expect(await getOutput(() => factory(16, 0))).toBe(expected)
   })
 
   test('async setup()', async () => {
@@ -192,8 +193,8 @@ describe('useId', () => {
       'v:1-0 v:1-1' + // inside second async subtree
       '</div>'
     // assert different async resolution order does not affect id stable-ness
-    expect(await getOutput(() => factory(10, 20))).toBe(expected)
-    expect(await getOutput(() => factory(20, 10))).toBe(expected)
+    expect(await getOutput(() => factory(0, 16))).toBe(expected)
+    expect(await getOutput(() => factory(16, 0))).toBe(expected)
   })
 
   test('deep nested', async () => {
@@ -239,4 +240,49 @@ describe('useId', () => {
     expect(await getOutput(() => factory())).toBe(expected)
     expect(await getOutput(() => factory())).toBe(expected)
   })
+
+  test('async component inside async setup, already resolved', async () => {
+    const factory = async (
+      delay1: number,
+      delay2: number,
+    ): Promise<FactoryRes> => {
+      const p1 = promiseWithDelay(null, delay1)
+      const p2 = promiseWithDelay(BasicComponentWithUseId, delay2)
+      const AsyncInner = defineAsyncComponent(() => p2)
+
+      const AsyncSetup = defineComponent({
+        async setup() {
+          await p1
+          return {}
+        },
+        render() {
+          return h(AsyncInner)
+        },
+      })
+
+      const app = createApp({
+        setup() {
+          const id1 = useId()
+          const id2 = useId()
+          return () =>
+            h(Suspense, null, {
+              default: h('div', [id1, ' ', id2, ' ', h(AsyncSetup)]),
+            })
+        },
+      })
+
+      // the async component may have already been resolved
+      await AsyncInner.__asyncLoader()
+      return [app, [p1, p2]]
+    }
+
+    const expected =
+      '<div>' +
+      'v:0 v:1 ' + // root
+      'v:0-0-0 v:0-0-1' + // async component inside async setup
+      '</div>'
+    // assert different async resolution order does not affect id stable-ness
+    expect(await getOutput(async () => factory(0, 16))).toBe(expected)
+    expect(await getOutput(() => factory(16, 0))).toBe(expected)
+  })
 })

+ 15 - 10
packages/runtime-core/__tests__/helpers/useModel.spec.ts

@@ -614,24 +614,23 @@ describe('useModel', () => {
   })
 
   test('set no change value', async () => {
-    let changeChildMsg: (() => void) | null = null
+    let changeChildMsg!: (val: string) => void
 
-    const compRender = vi.fn()
+    const setValue = vi.fn()
     const Comp = defineComponent({
       props: ['msg'],
       emits: ['update:msg'],
       setup(props) {
         const childMsg = useModel(props, 'msg')
-        changeChildMsg = () => {
-          childMsg.value = childMsg.value
-        }
+        changeChildMsg = (val: string) => (childMsg.value = val)
         return () => {
           return childMsg.value
         }
       },
     })
 
-    const msg = ref('HI')
+    const defaultVal = 'defaultVal'
+    const msg = ref(defaultVal)
     const Parent = defineComponent({
       setup() {
         return () =>
@@ -639,7 +638,7 @@ describe('useModel', () => {
             msg: msg.value,
             'onUpdate:msg': val => {
               msg.value = val
-              compRender()
+              setValue()
             },
           })
       },
@@ -648,8 +647,14 @@ describe('useModel', () => {
     const root = nodeOps.createElement('div')
     render(h(Parent), root)
 
-    expect(compRender).toBeCalledTimes(0)
-    changeChildMsg!()
-    expect(compRender).toBeCalledTimes(0)
+    expect(setValue).toBeCalledTimes(0)
+
+    changeChildMsg(defaultVal)
+    expect(setValue).toBeCalledTimes(0)
+
+    changeChildMsg('changed')
+    changeChildMsg(defaultVal)
+    expect(setValue).toBeCalledTimes(2)
+    expect(msg.value).toBe(defaultVal)
   })
 })

+ 48 - 0
packages/runtime-core/__tests__/hydration.spec.ts

@@ -688,6 +688,54 @@ describe('SSR hydration', () => {
     expect(container.innerHTML).toBe(`<span>1</span>`)
   })
 
+  // #6638
+  test('Suspense + async component', async () => {
+    let isSuspenseResolved = false
+    let isSuspenseResolvedInChild: any
+    const AsyncChild = defineAsyncComponent(() =>
+      Promise.resolve(
+        defineComponent({
+          setup() {
+            isSuspenseResolvedInChild = isSuspenseResolved
+            const count = ref(0)
+            return () =>
+              h(
+                'span',
+                {
+                  onClick: () => {
+                    count.value++
+                  },
+                },
+                count.value,
+              )
+          },
+        }),
+      ),
+    )
+    const { vnode, container } = mountWithHydration('<span>0</span>', () =>
+      h(
+        Suspense,
+        {
+          onResolve() {
+            isSuspenseResolved = true
+          },
+        },
+        () => h(AsyncChild),
+      ),
+    )
+    expect(vnode.el).toBe(container.firstChild)
+    // wait for hydration to finish
+    await new Promise(r => setTimeout(r))
+
+    expect(isSuspenseResolvedInChild).toBe(false)
+    expect(isSuspenseResolved).toBe(true)
+
+    // assert interaction
+    triggerEvent('click', container.querySelector('span')!)
+    await nextTick()
+    expect(container.innerHTML).toBe(`<span>1</span>`)
+  })
+
   test('Suspense (full integration)', async () => {
     const mountedCalls: number[] = []
     const asyncDeps: Promise<any>[] = []

+ 1 - 1
packages/runtime-core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-core",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "@vue/runtime-core",
   "main": "index.js",
   "module": "dist/runtime-core.esm-bundler.js",

+ 1 - 2
packages/runtime-core/src/apiAsyncComponent.ts

@@ -124,6 +124,7 @@ export function defineAsyncComponent<
 
     setup() {
       const instance = currentInstance!
+      markAsyncBoundary(instance)
 
       // already resolved
       if (resolvedComp) {
@@ -158,8 +159,6 @@ export function defineAsyncComponent<
                   })
                 : null
           })
-      } else {
-        markAsyncBoundary(instance)
       }
 
       const loaded = ref(false)

+ 2 - 1
packages/runtime-core/src/component.ts

@@ -94,6 +94,7 @@ import type { KeepAliveProps } from './components/KeepAlive'
 import type { BaseTransitionProps } from './components/BaseTransition'
 import type { DefineComponent } from './apiDefineComponent'
 import { markAsyncBoundary } from './helpers/useId'
+import { isAsyncWrapper } from './apiAsyncComponent'
 
 /**
  * Public utility type for extracting the instance type of a component.
@@ -863,7 +864,7 @@ function setupStatefulComponent(
 
     if (isPromise(setupResult)) {
       // async setup, mark as async boundary for useId()
-      markAsyncBoundary(instance)
+      if (!isAsyncWrapper(instance)) markAsyncBoundary(instance)
       setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
       if (isSSR) {
         // return the promise so server-renderer can wait on it

+ 8 - 5
packages/runtime-core/src/helpers/useModel.ts

@@ -33,7 +33,7 @@ export function useModel(
 
   const res = customRef((track, trigger) => {
     let localValue: any
-    let prevSetValue: any
+    let prevSetValue: any = EMPTY_OBJ
     let prevEmittedValue: any
 
     watchSyncEffect(() => {
@@ -51,7 +51,10 @@ export function useModel(
       },
 
       set(value) {
-        if (!hasChanged(value, localValue)) {
+        if (
+          !hasChanged(value, localValue) &&
+          !(prevSetValue !== EMPTY_OBJ && hasChanged(value, prevSetValue))
+        ) {
           return
         }
         const rawProps = i.vnode!.props
@@ -78,9 +81,9 @@ export function useModel(
         // updates and there will be no prop sync. However the local input state
         // may be out of sync, so we need to force an update here.
         if (
-          value !== emittedValue &&
-          value !== prevSetValue &&
-          emittedValue === prevEmittedValue
+          hasChanged(value, emittedValue) &&
+          hasChanged(value, prevSetValue) &&
+          !hasChanged(emittedValue, prevEmittedValue)
         ) {
           trigger()
         }

+ 6 - 3
packages/runtime-core/src/renderer.ts

@@ -1289,7 +1289,7 @@ function baseCreateRenderer(
     const componentUpdateFn = () => {
       if (!instance.isMounted) {
         let vnodeHook: VNodeHook | null | undefined
-        const { el, props } = initialVNode
+        const { el, props, type } = initialVNode
         const { bm, m, parent } = instance
         const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
 
@@ -1338,8 +1338,11 @@ function baseCreateRenderer(
             }
           }
 
-          if (isAsyncWrapperVNode) {
-            ;(initialVNode.type as ComponentOptions).__asyncLoader!().then(
+          if (
+            isAsyncWrapperVNode &&
+            !(type as ComponentOptions).__asyncResolved
+          ) {
+            ;(type as ComponentOptions).__asyncLoader!().then(
               // note: we are moving the render call into an async callback,
               // which means it won't track dependencies - but it's ok because
               // a server-rendered async wrapper is already in resolved state

+ 1 - 1
packages/runtime-dom/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/runtime-dom",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "@vue/runtime-dom",
   "main": "index.js",
   "module": "dist/runtime-dom.esm-bundler.js",

+ 1 - 1
packages/server-renderer/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/server-renderer",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "@vue/server-renderer",
   "main": "index.js",
   "module": "dist/server-renderer.esm-bundler.js",

+ 1 - 1
packages/shared/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/shared",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "internal utils shared across @vue packages",
   "main": "index.js",
   "module": "dist/shared.esm-bundler.js",

+ 1 - 1
packages/vue-compat/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vue/compat",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "Vue 3 compatibility build for Vue 2",
   "main": "index.js",
   "module": "dist/vue.runtime.esm-bundler.js",

+ 1 - 1
packages/vue/package.json

@@ -1,6 +1,6 @@
 {
   "name": "vue",
-  "version": "3.5.0-alpha.3",
+  "version": "3.5.0-alpha.4",
   "description": "The progressive JavaScript framework for building modern web UI.",
   "main": "index.js",
   "module": "dist/vue.runtime.esm-bundler.js",