Merge tag 'v3.5.0-alpha.4'

This commit is contained in:
三咲智子 Kevin Deng 2024-07-29 13:59:31 +08:00
commit f0405f1528
No known key found for this signature in database
20 changed files with 163 additions and 42 deletions

View File

@ -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)

View File

@ -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": {

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -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>[] = []

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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()
}

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",