From: daiwei Date: Sat, 26 Apr 2025 14:08:55 +0000 (+0800) Subject: wip: hydration for slots X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=42a421a93fb7661dbdb8f77d31c3a8a67f7a1151;p=thirdparty%2Fvuejs%2Fcore.git wip: hydration for slots --- diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts index 4247bc6fec..a3bf5cc219 100644 --- a/packages/compiler-vapor/src/generators/operation.ts +++ b/packages/compiler-vapor/src/generators/operation.ts @@ -44,7 +44,7 @@ export function genOperationWithInsertionState( ): CodeFragment[] { const [frag, push] = buildCodeFragment() if (isBlockOperation(oper) && oper.parent) { - push(...genInsertionstate(oper, context)) + push(...genInsertionState(oper, context)) } push(...genOperation(oper, context)) return frag @@ -152,7 +152,7 @@ export function genEffect( return frag } -function genInsertionstate( +function genInsertionState( operation: InsertionStateTypes, context: CodegenContext, ): CodeFragment[] { diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 016872b56a..14faf569c0 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -1531,6 +1531,20 @@ describe('Vapor Mode hydration', () => { `` + ``, ) + + data.value.splice(0, 1) + await nextTick() + expect(container.innerHTML).toBe( + `
` + + `` + + `` + + `b` + + `c` + + `d` + + `` + + `` + + `
`, + ) }) test('consecutive v-for with anchor insertion', async () => { @@ -1583,20 +1597,377 @@ describe('Vapor Mode hydration', () => { `` + ``, ) + + data.value.splice(0, 2) + await nextTick() + expect(container.innerHTML).toBe( + `
` + + `` + + `` + + `c` + + `d` + + `` + + `` + + `c` + + `d` + + `` + + `` + + `
`, + ) }) - // TODO wait for slots hydration support - test.todo('v-for on component', async () => {}) + test('v-for on component', async () => { + const { container, data } = await testHydration( + ``, + { + Child: ``, + }, + ref(['a', 'b', 'c']), + ) - // TODO wait for slots hydration support - test.todo('on fragment component', async () => {}) + expect(container.innerHTML).toBe( + `
` + + `` + + `
comp
` + + `
comp
` + + `
comp
` + + `` + + `
`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `
` + + `` + + `
comp
` + + `
comp
` + + `
comp
` + + `
comp
` + + `` + + `
`, + ) + }) + + test('v-for on component with slots', async () => { + const { container, data } = await testHydration( + ``, + { + Child: ``, + }, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `
` + + `` + + `a` + + `b` + + `c` + + `` + + `
`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `
` + + `` + + `a` + + `b` + + `c` + + `d` + + `` + + `
`, + ) + }) + + test('on fragment component', async () => { + const { container, data } = await testHydration( + ``, + { + Child: ``, + }, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `
` + + `` + + `
foo
-bar-` + + `
foo
-bar-` + + `
foo
-bar-` + + `` + + `
`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `
` + + `` + + `
foo
-bar-` + + `
foo
-bar-` + + `
foo
-bar-` + + `
foo
-bar-` + + `` + + `
`, + ) + }) // TODO wait for vapor TransitionGroup support // v-for inside TransitionGroup does not render as a fragment test.todo('v-for in TransitionGroup', async () => {}) }) - test.todo('slots') + describe('slots', () => { + test('basic slot', async () => { + const { data, container } = await testHydration( + ``, + { + Child: ``, + }, + ) + expect(container.innerHTML).toBe( + `foo`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `bar`, + ) + }) + + test('named slot', async () => { + const { data, container } = await testHydration( + ``, + { + Child: ``, + }, + ) + expect(container.innerHTML).toBe( + `foo`, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `bar`, + ) + }) + + test('named slot with v-if', async () => { + const { data, container } = await testHydration( + ``, + { + Child: ``, + }, + ) + expect(container.innerHTML).toBe( + `foo`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe(``) + }) + + test('named slot with v-if and v-for', async () => { + const data = reactive({ + show: true, + items: ['a', 'b', 'c'], + }) + const { container } = await testHydration( + ``, + { + Child: ``, + }, + data, + ) + expect(container.innerHTML).toBe( + `` + + `abc` + + `` + + ``, + ) + + data.show = false + await nextTick() + expect(container.innerHTML).toBe( + ``, + ) + }) + + test('with anchor insertion', async () => { + const { data, container } = await testHydration( + ``, + { + Child: ``, + }, + ) + expect(container.innerHTML).toBe( + `` + + `` + + `foo` + + `` + + `` + + ``, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `` + + `` + + `bar` + + `` + + `` + + ``, + ) + }) + + test('with multi level anchor insertion', async () => { + const { data, container } = await testHydration( + ``, + { + Child: ` + `, + }, + ) + expect(container.innerHTML).toBe( + `` + + `
` + + `
` + + `` + + `` + + `foo` + + `` + + `` + + `` + + `
` + + ``, + ) + + data.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `` + + `
` + + `
` + + `` + + `` + + `bar` + + `` + + `` + + `` + + `
` + + ``, + ) + }) + + // problem is next child is incorrect after slot + test.todo('mixed slot and text node', async () => { + const data = reactive({ + text: 'foo', + msg: 'hi', + }) + const { container } = await testHydration( + ``, + { + Child: ``, + }, + data, + ) + + expect(container.innerHTML).toMatchInlineSnapshot( + `"
foohi
"`, + ) + }) + + test.todo('mixed slot and element', async () => { + const data = reactive({ + text: 'foo', + msg: 'hi', + }) + const { container } = await testHydration( + ``, + { + Child: ``, + }, + data, + ) + + expect(container.innerHTML).toMatchInlineSnapshot( + `"
foo
hi
"`, + ) + }) + + // mixed slot and component + // mixed slot and fragment component + // mixed slot and v-if + // mixed slot and v-for + }) // test('element with ref', () => { // const el = ref() diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index c0727cb5d9..c846cc8724 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -1,4 +1,4 @@ -import { isArray, isVaporFragmentEndAnchor } from '@vue/shared' +import { isArray } from '@vue/shared' import { type VaporComponentInstance, isVaporComponent, @@ -100,10 +100,10 @@ export class DynamicFragment extends VaporFragment { } else { // find next sibling dynamic fragment end anchor const anchor = nextVaporFragmentAnchor(currentHydrationNode!, label)! - if (anchor && isVaporFragmentEndAnchor(anchor)) { + if (anchor) { this.anchor = anchor } else if (__DEV__) { - // TODO warning + // TODO warning, should not happen warn(`DynamicFragment anchor not found...`) } } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 74296e0946..093ed7fb08 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,11 +1,22 @@ -import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' +import { + EMPTY_OBJ, + NO, + SLOT_ANCHOR_LABEL, + hasOwn, + isArray, + isFunction, +} from '@vue/shared' import { type Block, type BlockFn, DynamicFragment, insert } from './block' import { rawPropsProxyHandlers } from './componentProps' import { currentInstance, isRef } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' import { renderEffect } from './renderEffect' -import { insertionAnchor, insertionParent } from './insertionState' -import { isHydrating, locateHydrationNode } from './dom/hydration' +import { + insertionAnchor, + insertionParent, + resetInsertionState, +} from './insertionState' +import { isHydrating } from './dom/hydration' export type RawSlots = Record & { $?: DynamicSlotSource[] @@ -94,9 +105,7 @@ export function createSlot( ): Block { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor - if (isHydrating) { - locateHydrationNode() - } + if (!isHydrating) resetInsertionState() const instance = currentInstance as VaporComponentInstance const rawSlots = instance.rawSlots @@ -115,7 +124,7 @@ export function createSlot( fallback, ) } else { - fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment() + fragment = new DynamicFragment(SLOT_ANCHOR_LABEL) const isDynamicName = isFunction(name) const renderSlot = () => { const slot = getSlot(rawSlots, isFunction(name) ? name() : name) diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index f290954b7f..2506a5c8fb 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -42,7 +42,7 @@ export function withHydration(container: ParentNode, fn: () => void): void { } export let adoptTemplate: (node: Node, template: string) => Node | null -export let locateHydrationNode: (isFragment?: boolean) => void +export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void type Anchor = Comment & { // cached matching fragment start to avoid repeated traversal @@ -94,10 +94,16 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) { } else { node = insertionParent ? insertionParent.lastChild : currentHydrationNode + // if current node is fragment start anchor, find the next one + if (node && isComment(node, '[')) { + node = node.nextSibling + } // if the last child is a vapor fragment end anchor, find the previous one - if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) { - let previous = node.previousSibling - if (previous) node = previous + else if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) { + node = node.previousSibling + if (__DEV__ && !node) { + // TODO warning, should not happen + } } if (node && isComment(node, ']')) { diff --git a/packages/server-renderer/__tests__/render.spec.ts b/packages/server-renderer/__tests__/render.spec.ts index d0a5223b2f..03b7402ff9 100644 --- a/packages/server-renderer/__tests__/render.spec.ts +++ b/packages/server-renderer/__tests__/render.spec.ts @@ -446,7 +446,7 @@ function testRender(type: string, render: typeof renderToString) { ).toBe( `
parent
` + `from slot` + - `
`, + ``, ) // test fallback @@ -461,7 +461,7 @@ function testRender(type: string, render: typeof renderToString) { }), ), ).toBe( - `
parent
fallback
`, + `
parent
fallback
`, ) }) @@ -507,7 +507,7 @@ function testRender(type: string, render: typeof renderToString) { ).toBe( `
parent
` + `from slot` + - `
`, + ``, ) }) @@ -525,7 +525,7 @@ function testRender(type: string, render: typeof renderToString) { expect(await render(app)).toBe( `
parent
` + `from slot` + - `
`, + ``, ) }) @@ -572,7 +572,7 @@ function testRender(type: string, render: typeof renderToString) { }) expect(await render(app)).toBe( - `
hello
`, + `
hello
`, ) }) @@ -593,7 +593,7 @@ function testRender(type: string, render: typeof renderToString) { expect(await render(app)).toBe( // should only have a single fragment - `
`, + `
`, ) }) @@ -614,7 +614,7 @@ function testRender(type: string, render: typeof renderToString) { expect(await render(app)).toBe( // should only have a single fragment - `
fallback
`, + `
fallback
`, ) }) }) diff --git a/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts b/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts index b685fbfe1a..181720c5b3 100644 --- a/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts +++ b/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts @@ -15,7 +15,7 @@ describe('ssr: dynamic component', () => { }), ), ).toBe( - `
slot
`, + `
slot
`, ) }) @@ -63,7 +63,7 @@ describe('ssr: dynamic component', () => { }), ), ).toBe( - `
testslot
`, + `
testslot
`, ) }) diff --git a/packages/server-renderer/__tests__/ssrScopeId.spec.ts b/packages/server-renderer/__tests__/ssrScopeId.spec.ts index 4ceb865fb5..c4135e498b 100644 --- a/packages/server-renderer/__tests__/ssrScopeId.spec.ts +++ b/packages/server-renderer/__tests__/ssrScopeId.spec.ts @@ -68,7 +68,9 @@ describe('ssr: scopedId runtime behavior', () => { } const result = await renderToString(createApp(Comp)) - expect(result).toBe(`
`) + expect(result).toBe( + `
`, + ) }) // #2892 @@ -150,8 +152,8 @@ describe('ssr: scopedId runtime behavior', () => { const result = await renderToString(createApp(Root)) expect(result).toBe( `
` + - `
` + - `
`, + `
` + + ``, ) }) @@ -265,8 +267,8 @@ describe('ssr: scopedId runtime behavior', () => { const result = await renderToString(createApp(Root)) expect(result).toBe( `
` + - `
` + - `
`, + `
` + + ``, ) }) }) diff --git a/packages/server-renderer/__tests__/ssrSlot.spec.ts b/packages/server-renderer/__tests__/ssrSlot.spec.ts index 0e3e353564..d17e34bc7c 100644 --- a/packages/server-renderer/__tests__/ssrSlot.spec.ts +++ b/packages/server-renderer/__tests__/ssrSlot.spec.ts @@ -16,7 +16,7 @@ describe('ssr: slot', () => { template: `hello`, }), ), - ).toBe(`
hello
`) + ).toBe(`
hello
`) }) test('element slot', async () => { @@ -27,7 +27,7 @@ describe('ssr: slot', () => { template: `
hi
`, }), ), - ).toBe(`
hi
`) + ).toBe(`
hi
`) }) test('empty slot', async () => { @@ -42,7 +42,7 @@ describe('ssr: slot', () => { template: `