From: daiwei Date: Wed, 23 Jul 2025 10:07:22 +0000 (+0800) Subject: wip: render fallback nodes for vfor X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=a65da3aee915014b9de5652481a290d87b517d3f;p=thirdparty%2Fvuejs%2Fcore.git wip: render fallback nodes for vfor --- diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 608a0ff556..ea9a3f8c8d 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -1,7 +1,9 @@ // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`. import { + child, createComponent, + createFor, createForSlots, createIf, createSlot, @@ -12,10 +14,15 @@ import { renderEffect, template, } from '../src' -import { currentInstance, nextTick, ref } from '@vue/runtime-dom' +import { + currentInstance, + nextTick, + ref, + toDisplayString, +} from '@vue/runtime-dom' import { makeRender } from './_utils' import type { DynamicSlot } from '../src/componentSlots' -import { setElementText } from '../src/dom/prop' +import { setElementText, setText } from '../src/dom/prop' const define = makeRender() @@ -562,7 +569,7 @@ describe('component: slots', () => { expect(html()).toBe('fallback') }) - test('render fallback with nested v-if ', async () => { + test('render fallback with nested v-if', async () => { const Child = { setup() { return createSlot('default', null, () => @@ -620,5 +627,101 @@ describe('component: slots', () => { await nextTick() expect(html()).toBe('content') }) + + test('render fallback with v-for', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const items = ref([1]) + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + const n2 = createFor( + () => items.value, + for_item0 => { + const n4 = template(' ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(for_item0.value)), + ) + return n4 + }, + ) + return n2 + }, + }) + }, + }).render() + + expect(html()).toBe('1') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.push(2) + await nextTick() + expect(html()).toBe('2') + }) + + test('render fallback with v-for (empty source)', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const items = ref([]) + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + const n2 = createFor( + () => items.value, + for_item0 => { + const n4 = template(' ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(for_item0.value)), + ) + return n4 + }, + ) + return n2 + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + + items.value.push(1) + await nextTick() + expect(html()).toBe('1') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.push(2) + await nextTick() + expect(html()).toBe('2') + }) }) }) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 9ffdf6dca5..ac2a94f092 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -15,8 +15,10 @@ import { isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode } from './dom/node' import { type Block, + ForFragment, VaporFragment, insert, + remove, remove as removeBlock, } from './block' import { warn } from '@vue/runtime-dom' @@ -77,7 +79,7 @@ export const createFor = ( setup?: (_: { createSelector: (source: () => any) => (cb: () => void) => void }) => void, -): VaporFragment => { +): ForFragment => { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor if (isHydrating) { @@ -94,7 +96,7 @@ export const createFor = ( let currentKey: any // TODO handle this in hydration const parentAnchor = __DEV__ ? createComment('for') : createTextNode() - const frag = new VaporFragment(oldBlocks) + const frag = new ForFragment(oldBlocks) const instance = currentInstance! const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE) const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT) @@ -112,6 +114,7 @@ export const createFor = ( const newLength = source.values.length const oldLength = oldBlocks.length newBlocks = new Array(newLength) + let isFallback = false const prevSub = setActiveSub() @@ -123,6 +126,11 @@ export const createFor = ( } else { parent = parent || parentAnchor!.parentNode if (!oldLength) { + // remove fallback nodes + if (frag.fallback && (frag.nodes[0] as Block[]).length > 0) { + remove(frag.nodes[0], parent!) + } + // fast path for all new for (let i = 0; i < newLength; i++) { mount(source, i) @@ -140,6 +148,13 @@ export const createFor = ( parent!.textContent = '' parent!.appendChild(parentAnchor) } + + // render fallback nodes + if (frag.fallback) { + insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor) + oldBlocks = [] + isFallback = true + } } else if (!getKey) { // unkeyed fast path const commonLength = Math.min(newLength, oldLength) @@ -324,11 +339,12 @@ export const createFor = ( } } - frag.nodes = [(oldBlocks = newBlocks)] - if (parentAnchor) { - frag.nodes.push(parentAnchor) + if (!isFallback) { + frag.nodes = [(oldBlocks = newBlocks)] + if (parentAnchor) { + frag.nodes.push(parentAnchor) + } } - setActiveSub(prevSub) } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 2a78048f8b..0271c1c008 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -18,17 +18,24 @@ export type Block = export type BlockFn = (...args: any[]) => Block -export class VaporFragment { - nodes: Block +export class VaporFragment { + nodes: T anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void + fallback?: BlockFn - constructor(nodes: Block) { + constructor(nodes: T) { this.nodes = nodes } } +export class ForFragment extends VaporFragment { + constructor(nodes: Block[]) { + super(nodes) + } +} + export class DynamicFragment extends VaporFragment { anchor: Node scope: EffectScope | undefined @@ -65,16 +72,18 @@ export class DynamicFragment extends VaporFragment { this.nodes = [] } - if (this.fallback && !isValidBlock(this.nodes)) { + if (this.fallback) { parent && remove(this.nodes, parent) - // handle nested dynamic fragment - if (isFragment(this.nodes)) { - renderFallback(this.nodes, this.fallback, key) - } else { - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] - } + const scope = this.scope || (this.scope = new EffectScope()) + scope.run(() => { + // handle nested fragment + if (isFragment(this.nodes)) { + ensureFallback(this.nodes, this.fallback!) + } else if (!isValidBlock(this.nodes)) { + this.nodes = this.fallback!() || [] + } + }) + parent && insert(this.nodes, parent, this.anchor) } @@ -82,19 +91,22 @@ export class DynamicFragment extends VaporFragment { } } -function renderFallback( - fragment: VaporFragment, - fallback: BlockFn, - key: any, -): void { +function ensureFallback(fragment: VaporFragment, fallback: BlockFn): void { + if (!fragment.fallback) fragment.fallback = fallback + if (fragment instanceof DynamicFragment) { const nodes = fragment.nodes if (isFragment(nodes)) { - renderFallback(nodes, fallback, key) - } else { - if (!fragment.fallback) fragment.fallback = fallback - fragment.update(fragment.fallback, key) + ensureFallback(nodes, fallback) + } else if (!isValidBlock(nodes)) { + fragment.update(fragment.fallback) } + } else if (fragment instanceof ForFragment) { + if (!isValidBlock(fragment.nodes[0])) { + fragment.nodes[0] = [fallback() || []] as Block[] + } + } else { + // vdom slots } } @@ -117,7 +129,7 @@ export function isValidBlock(block: Block): boolean { } else if (isVaporComponent(block)) { return isValidBlock(block.block) } else if (isArray(block)) { - return block.length > 0 && block.every(isValidBlock) + return block.length > 0 && block.some(isValidBlock) } else { // fragment return isValidBlock(block.nodes)