]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: render fallback nodes for vfor
authordaiwei <daiwei521@126.com>
Wed, 23 Jul 2025 10:07:22 +0000 (18:07 +0800)
committerdaiwei <daiwei521@126.com>
Wed, 23 Jul 2025 13:41:35 +0000 (21:41 +0800)
packages/runtime-vapor/__tests__/componentSlots.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/block.ts

index 608a0ff556855ac482985ad6fed882f1c014de76..ea9a3f8c8d7150763db1ddaa1d5f75efc53a0552 100644 (file)
@@ -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<any>()
 
@@ -562,7 +569,7 @@ describe('component: slots', () => {
       expect(html()).toBe('fallback<!--if--><!--slot-->')
     })
 
-    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<!--if--><!--if--><!--slot-->')
     })
+
+    test('render fallback with v-for', async () => {
+      const Child = {
+        setup() {
+          return createSlot('default', null, () =>
+            document.createTextNode('fallback'),
+          )
+        },
+      }
+
+      const items = ref<number[]>([1])
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: () => {
+              const n2 = createFor(
+                () => items.value,
+                for_item0 => {
+                  const n4 = template('<span> </span>')() as any
+                  const x4 = child(n4) as any
+                  renderEffect(() =>
+                    setText(x4, toDisplayString(for_item0.value)),
+                  )
+                  return n4
+                },
+              )
+              return n2
+            },
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('<span>1</span><!--for--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.push(2)
+      await nextTick()
+      expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
+    })
+
+    test('render fallback with v-for (empty source)', async () => {
+      const Child = {
+        setup() {
+          return createSlot('default', null, () =>
+            document.createTextNode('fallback'),
+          )
+        },
+      }
+
+      const items = ref<number[]>([])
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: () => {
+              const n2 = createFor(
+                () => items.value,
+                for_item0 => {
+                  const n4 = template('<span> </span>')() as any
+                  const x4 = child(n4) as any
+                  renderEffect(() =>
+                    setText(x4, toDisplayString(for_item0.value)),
+                  )
+                  return n4
+                },
+              )
+              return n2
+            },
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.push(1)
+      await nextTick()
+      expect(html()).toBe('<span>1</span><!--for--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.push(2)
+      await nextTick()
+      expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
+    })
   })
 })
index 9ffdf6dca571ec2217632f6e10dc85abc8c25095..ac2a94f09267888afb527dd5a5a87a8416369a50 100644 (file)
@@ -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)
   }
 
index 2a78048f8b26bab135624c70b7c3f0966d7d2a5f..0271c1c0087992d0de2dce80e46a99d649122914 100644 (file)
@@ -18,17 +18,24 @@ export type Block =
 
 export type BlockFn = (...args: any[]) => Block
 
-export class VaporFragment {
-  nodes: Block
+export class VaporFragment<T extends Block = Block> {
+  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<Block[]> {
+  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)