From: daiwei Date: Fri, 25 Apr 2025 09:08:07 +0000 (+0800) Subject: wip: v-for hydration X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e6e016016fd6f463cd41be3c46419a5de78b220f;p=thirdparty%2Fvuejs%2Fcore.git wip: v-for hydration --- diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts index 53295fb03d..73d4331a7d 100644 --- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts @@ -15,7 +15,7 @@ describe('transition-group', () => { _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) }) - _push(\`\`) + _push(\`\`) }" `) }) @@ -33,7 +33,7 @@ describe('transition-group', () => { _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) }) - _push(\`\`) + _push(\`\`) }" `) }) @@ -52,6 +52,7 @@ describe('transition-group', () => { _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) }) + _push(\`\`) if (false) { _push(\`
\`) _push(\`\`) @@ -75,7 +76,7 @@ describe('transition-group', () => { _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) }) - _push(\`\`) + _push(\`\`) }" `) }) @@ -97,7 +98,7 @@ describe('transition-group', () => { _ssrRenderList(_ctx.list, (i) => { _push(\`
\`) }) - _push(\`\`) + _push(\`\`) }" `) }) @@ -119,9 +120,11 @@ describe('transition-group', () => { _ssrRenderList(10, (i) => { _push(\`
\`) }) + _push(\`\`) _ssrRenderList(10, (i) => { _push(\`
\`) }) + _push(\`\`) if (_ctx.ok) { _push(\`
ok
\`) _push(\`\`) diff --git a/packages/compiler-ssr/src/transforms/ssrVFor.ts b/packages/compiler-ssr/src/transforms/ssrVFor.ts index 6537eee828..8276507850 100644 --- a/packages/compiler-ssr/src/transforms/ssrVFor.ts +++ b/packages/compiler-ssr/src/transforms/ssrVFor.ts @@ -13,6 +13,7 @@ import { processChildrenAsStatement, } from '../ssrCodegenTransform' import { SSR_RENDER_LIST } from '../runtimeHelpers' +import { FOR_ANCHOR_LABEL } from '@vue/shared' // Plugin for the first transform pass, which simply constructs the AST node export const ssrTransformFor: NodeTransform = @@ -48,5 +49,8 @@ export function ssrProcessFor( ) if (!disableNestedFragments) { context.pushStringPart(``) + } else { + // add anchor for non-fragment v-for + context.pushStringPart(``) } } diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index a37076ce16..c15bd68542 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -1123,6 +1123,73 @@ describe('Vapor Mode hydration', () => { }) }) + test('on fragment component', async () => { + runWithEnv(isProd, async () => { + const data = ref(true) + const { container } = await testHydration( + ``, + { + Child: ``, + }, + data, + ) + expect(container.innerHTML).toBe( + `
` + + `
true
-true-` + + `` + + `
`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `
` + `` + `` + `
`, + ) + }) + }) + + test('on fragment component with anchor insertion', async () => { + runWithEnv(isProd, async () => { + const data = ref(true) + const { container } = await testHydration( + ``, + { + Child: ``, + }, + data, + ) + expect(container.innerHTML).toBe( + `
` + + `` + + `
true
-true-` + + `` + + `` + + `
`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toBe( + `
` + + `` + + `` + + `` + + `` + + `
`, + ) + }) + }) + test('consecutive v-if on fragment component with anchor insertion', async () => { runWithEnv(isProd, async () => { const data = ref(true) @@ -1311,7 +1378,168 @@ describe('Vapor Mode hydration', () => { } }) - test.todo('for') + describe('for', () => { + test('basic v-for', async () => { + const { container, data } = await testHydration( + ``, + undefined, + 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('v-for with text node', async () => { + const { container, data } = await testHydration( + ``, + undefined, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `
abc
`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `
abcd
`, + ) + }) + + test('v-for with anchor insertion', async () => { + const { container, data } = await testHydration( + ``, + undefined, + 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('consecutive v-for with anchor insertion', async () => { + const { container, data } = await testHydration( + ``, + undefined, + ref(['a', 'b', 'c']), + ) + expect(container.innerHTML).toBe( + `
` + + `` + + `` + + `a` + + `b` + + `c` + + `` + + `` + + `` + + `a` + + `b` + + `c` + + `` + + `` + + `` + + `
`, + ) + + data.value.push('d') + await nextTick() + expect(container.innerHTML).toBe( + `
` + + `` + + `` + + `a` + + `b` + + `c` + + `d` + + `` + + `` + + `` + + `a` + + `b` + + `c` + + `d` + + `` + + `` + + `` + + `
`, + ) + }) + + // TODO wait for slots hydration support + test.todo('v-for on component', async () => {}) + + // TODO wait for slots hydration support + test.todo('on fragment component', async () => {}) + + // 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') diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 0cd8317532..b8016a64de 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -9,8 +9,14 @@ import { shallowRef, toReactive, } from '@vue/reactivity' -import { getSequence, isArray, isObject, isString } from '@vue/shared' -import { createComment, createTextNode } from './dom/node' +import { + FOR_ANCHOR_LABEL, + getSequence, + isArray, + isObject, + isString, +} from '@vue/shared' +import { createComment, createTextNode, nextSiblingAnchor } from './dom/node' import { type Block, VaporFragment, @@ -22,8 +28,17 @@ import { currentInstance, isVaporComponent } from './component' import type { DynamicSlot } from './componentSlots' import { renderEffect } from './renderEffect' import { VaporVForFlags } from '../../shared/src/vaporFlags' -import { isHydrating, locateHydrationNode } from './dom/hydration' -import { insertionAnchor, insertionParent } from './insertionState' +import { + currentHydrationNode, + isComment, + isHydrating, + locateHydrationNode, +} from './dom/hydration' +import { + insertionAnchor, + insertionParent, + resetInsertionState, +} from './insertionState' class ForBlock extends VaporFragment { scope: EffectScope | undefined @@ -71,15 +86,24 @@ export const createFor = ( const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor if (isHydrating) { - locateHydrationNode() + locateHydrationNode(true) + } else { + resetInsertionState() } let isMounted = false let oldBlocks: ForBlock[] = [] let newBlocks: ForBlock[] let parent: ParentNode | undefined | null - // TODO handle this in hydration - const parentAnchor = __DEV__ ? createComment('for') : createTextNode() + const parentAnchor = isHydrating + ? // Use fragment end anchor if available, otherwise use the specific for anchor. + nextSiblingAnchor( + currentHydrationNode!, + isComment(currentHydrationNode!, '[') ? ']' : FOR_ANCHOR_LABEL, + )! + : __DEV__ + ? createComment('for') + : createTextNode() const frag = new VaporFragment(oldBlocks) const instance = currentInstance! const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index b0e8e7d528..4588f4887a 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -10,7 +10,6 @@ import { disableHydrationNodeLookup, enableHydrationNodeLookup, next, - prev, } from './node' import { isDynamicFragmentEndAnchor } from '@vue/shared' @@ -98,7 +97,7 @@ function locateHydrationNodeImpl(isFragment?: boolean) { // if the last child is a comment, it is the anchor for the fragment // so it need to find the previous node if (isFragment && node && isDynamicFragmentEndAnchor(node)) { - let previous = prev(node) + let previous = node.previousSibling //prev(node) if (previous) node = previous } diff --git a/packages/runtime-vapor/src/dom/node.ts b/packages/runtime-vapor/src/dom/node.ts index 2a546c69e2..6b9b63c5c2 100644 --- a/packages/runtime-vapor/src/dom/node.ts +++ b/packages/runtime-vapor/src/dom/node.ts @@ -105,6 +105,7 @@ export function disableHydrationNodeLookup(): void { } /*! #__NO_SIDE_EFFECTS__ */ +// TODO check if this is still needed export function prev(node: Node): Node | null { // process dynamic node (...) as a single one if (isComment(node, DYNAMIC_END_ANCHOR_LABEL)) { @@ -145,6 +146,9 @@ export function nextSiblingAnchor( anchorLabel: string, ): Comment | null { node = handleWrappedNode(node) + if (isComment(node, anchorLabel)) { + return node as Comment + } let n = node.nextSibling while (n) {