]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(runtime-vapor): template fragment (#100)
authorRizumu Ayaka <rizumu@ayaka.moe>
Mon, 29 Jan 2024 20:15:52 +0000 (04:15 +0800)
committerGitHub <noreply@github.com>
Mon, 29 Jan 2024 20:15:52 +0000 (04:15 +0800)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
packages/runtime-vapor/__tests__/if.spec.ts
packages/runtime-vapor/__tests__/template.spec.ts
packages/runtime-vapor/src/dom.ts
packages/runtime-vapor/src/if.ts
packages/runtime-vapor/src/render.ts
packages/runtime-vapor/src/template.ts

index 5f8012ca9775cfc5d9ca43640f412ea68e6256a4..ee4b87907e633a9a2c94d925af1484827bce3477 100644 (file)
@@ -1,7 +1,9 @@
 import { defineComponent } from 'vue'
 import {
+  append,
   children,
   createIf,
+  fragment,
   insert,
   nextTick,
   ref,
@@ -10,7 +12,6 @@ import {
   setText,
   template,
 } from '../src'
-import { NOOP } from '@vue/shared'
 import type { Mock } from 'vitest'
 
 let host: HTMLElement
@@ -103,4 +104,65 @@ describe('createIf', () => {
     expect(spyIfFn!).toHaveBeenCalledTimes(1)
     expect(spyElseFn!).toHaveBeenCalledTimes(2)
   })
+
+  test('should handle nested template', async () => {
+    // mock this template:
+    //  <template v-if="ok1">
+    //    Hello <template v-if="ok2">Vapor</template>
+    //  </template>
+
+    const ok1 = ref(true)
+    const ok2 = ref(true)
+
+    const t0 = template('Vapor')
+    const t1 = template('Hello ')
+    const t2 = fragment()
+    render(
+      defineComponent({
+        setup() {
+          // render
+          return (() => {
+            const n0 = t2()
+            append(
+              n0,
+              createIf(
+                () => ok1.value,
+                () => {
+                  const n2 = t1()
+                  append(
+                    n2,
+                    createIf(
+                      () => ok2.value,
+                      () => t0(),
+                    ),
+                  )
+                  return n2
+                },
+              ),
+            )
+            return n0
+          })()
+        },
+      }) as any,
+      {},
+      '#host',
+    )
+    expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
+
+    ok1.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--if-->')
+
+    ok1.value = true
+    await nextTick()
+    expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
+
+    ok2.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('Hello <!--if--><!--if-->')
+
+    ok1.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--if-->')
+  })
 })
index 16ac338337ce0d1081d7fd8448a83a21f2b1d165..84c45783d177656e253b2130204f28f924c21e34 100644 (file)
@@ -4,12 +4,12 @@ describe('api: template', () => {
   test('create element', () => {
     const t = template('<div>')
     const root = t()
-    expect(root).toBeInstanceOf(DocumentFragment)
-    expect(root.childNodes[0]).toBeInstanceOf(HTMLDivElement)
+    expect(root).toBeInstanceOf(Array)
+    expect(root[0]).toBeInstanceOf(HTMLDivElement)
 
-    const div2 = t()
-    expect(div2).toBeInstanceOf(DocumentFragment)
-    expect(div2).not.toBe(root)
+    const root2 = t()
+    expect(root2).toBeInstanceOf(Array)
+    expect(root2).not.toBe(root)
   })
 
   test('create fragment', () => {
index 4fe2f06351bbae7ec5f6586a2576589572f0b853..eedb73722d4ba252f065a110385340f29ac336b7 100644 (file)
@@ -5,85 +5,69 @@ export * from './dom/patchProp'
 export * from './dom/templateRef'
 export * from './dom/on'
 
-export function insert(block: Block, parent: Node, anchor: Node | null = null) {
+function normalizeBlock(block: Block): Node[] {
+  const nodes: Node[] = []
   if (block instanceof Node) {
-    parent.insertBefore(block, anchor)
+    nodes.push(block)
   } else if (isArray(block)) {
-    for (const child of block) insert(child, parent, anchor)
-  } else {
-    insert(block.nodes, parent, anchor)
-    block.anchor && parent.insertBefore(block.anchor, anchor)
+    block.forEach(child => nodes.push(...normalizeBlock(child)))
+  } else if (block) {
+    nodes.push(...normalizeBlock(block.nodes))
+    block.anchor && nodes.push(block.anchor)
   }
+  return nodes
 }
 
-export function prepend(parent: ParentBlock, ...blocks: Block[]) {
-  const nodes: Node[] = []
-
-  for (const block of blocks) {
-    if (block instanceof Node) {
-      nodes.push(block)
-    } else if (isArray(block)) {
-      prepend(parent, ...block)
+export function insert(
+  block: Block,
+  parent: ParentBlock,
+  anchor: Node | null = null,
+) {
+  if (isArray(parent)) {
+    const index = anchor ? parent.indexOf(anchor) : -1
+    if (index > -1) {
+      parent.splice(index, 0, block)
     } else {
-      prepend(parent, block.nodes)
-      block.anchor && prepend(parent, block.anchor)
+      parent.push(block)
     }
+  } else {
+    normalizeBlock(block).forEach(node => parent.insertBefore(node, anchor))
   }
+}
 
-  if (!nodes.length) return
-
-  if (parent instanceof Node) {
-    // TODO use insertBefore for better performance https://jsbench.me/rolpg250hh/1
-    parent.prepend(...nodes)
-  } else if (isArray(parent)) {
-    parent.unshift(...nodes)
+export function prepend(parent: ParentBlock, ...blocks: Block[]) {
+  if (isArray(parent)) {
+    parent.unshift(...blocks)
+  } else {
+    parent.prepend(...normalizeBlock(blocks))
   }
 }
 
 export function append(parent: ParentBlock, ...blocks: Block[]) {
-  const nodes: Node[] = []
-
-  for (const block of blocks) {
-    if (block instanceof Node) {
-      nodes.push(block)
-    } else if (isArray(block)) {
-      append(parent, ...block)
-    } else {
-      append(parent, block.nodes)
-      block.anchor && append(parent, block.anchor)
-    }
-  }
-
-  if (!nodes.length) return
-
-  if (parent instanceof Node) {
-    // TODO use insertBefore for better performance
-    parent.append(...nodes)
-  } else if (isArray(parent)) {
-    parent.push(...nodes)
+  if (isArray(parent)) {
+    parent.push(...blocks)
+  } else {
+    parent.append(...normalizeBlock(blocks))
   }
 }
 
-export function remove(block: Block, parent: ParentNode) {
-  if (block instanceof DocumentFragment) {
-    remove(Array.from(block.childNodes), parent)
-  } else if (block instanceof Node) {
-    parent.removeChild(block)
-  } else if (isArray(block)) {
-    for (const child of block) remove(child, parent)
+export function remove(block: Block, parent: ParentBlock) {
+  if (isArray(parent)) {
+    const index = parent.indexOf(block)
+    if (index > -1) {
+      parent.splice(index, 1)
+    }
   } else {
-    remove(block.nodes, parent)
-    block.anchor && parent.removeChild(block.anchor)
+    normalizeBlock(block).forEach(node => parent.removeChild(node))
   }
 }
 
 type Children = Record<number, [ChildNode, Children]>
-export function children(n: Node): Children {
+export function children(nodes: ChildNode[]): Children {
   const result: Children = {}
-  const array = Array.from(n.childNodes)
-  for (let i = 0; i < array.length; i++) {
-    const n = array[i]
-    result[i] = [n, children(n)]
+  for (let i = 0; i < nodes.length; i++) {
+    const n = nodes[i]
+    result[i] = [n, children(Array.from(n.childNodes))]
   }
   return result
 }
index a5072ed72fcfd22dae6bc4e1aad3ce008f4f362c..e2b50f26642eb41859406e45904c3a0e94ce0aa3 100644 (file)
@@ -1,8 +1,10 @@
 import { renderWatch } from './renderWatch'
-import { type BlockFn, type Fragment, fragmentKey } from './render'
-import { effectScope, onEffectCleanup } from '@vue/reactivity'
+import { type Block, type Fragment, fragmentKey } from './render'
+import { type EffectScope, effectScope } from '@vue/reactivity'
 import { createComment, createTextNode, insert, remove } from './dom'
 
+type BlockFn = () => Block
+
 export const createIf = (
   condition: () => any,
   b1: BlockFn,
@@ -11,8 +13,14 @@ export const createIf = (
 ): Fragment => {
   let branch: BlockFn | undefined
   let parent: ParentNode | undefined | null
+  let block: Block | undefined
+  let scope: EffectScope | undefined
   const anchor = __DEV__ ? createComment('if') : createTextNode('')
-  const fragment: Fragment = { nodes: [], anchor, [fragmentKey]: true }
+  const fragment: Fragment = {
+    nodes: [],
+    anchor,
+    [fragmentKey]: true,
+  }
 
   // TODO: SSR
   // if (isHydrating) {
@@ -24,23 +32,16 @@ export const createIf = (
     () => !!condition(),
     value => {
       parent ||= anchor.parentNode
+      if (block) {
+        scope!.stop()
+        remove(block, parent!)
+      }
       if ((branch = value ? b1 : b2)) {
-        let scope = effectScope()
-        let block = scope.run(branch)!
-
-        if (block instanceof DocumentFragment) {
-          block = Array.from(block.childNodes)
-        }
-        fragment.nodes = block
-
+        scope = effectScope()
+        fragment.nodes = block = scope.run(branch)!
         parent && insert(block, parent, anchor)
-
-        onEffectCleanup(() => {
-          parent ||= anchor.parentNode
-          scope.stop()
-          remove(block, parent!)
-        })
       } else {
+        scope = block = undefined
         fragment.nodes = []
       }
     },
index 8e81cab214c672f002f6d782a4854552ecde6ee0..8bae599530fcf087b28a9b763627534d83f3fdc3 100644 (file)
@@ -15,13 +15,12 @@ import { queuePostRenderEffect } from './scheduler'
 export const fragmentKey = Symbol('fragment')
 
 export type Block = Node | Fragment | Block[]
-export type ParentBlock = ParentNode | Node[]
+export type ParentBlock = ParentNode | Block[]
 export type Fragment = {
   nodes: Block
   anchor?: Node
   [fragmentKey]: true
 }
-export type BlockFn = (props?: any) => Block
 
 export function render(
   comp: Component,
index 8b505a6ec88f317c6c3f218c8254c4227817612f..17ab8e5a4b56e1f3565bac2e5318631aedc1ab09 100644 (file)
@@ -1,4 +1,4 @@
-export const template = (str: string): (() => DocumentFragment) => {
+export function template(str: string): () => ChildNode[] {
   let cached = false
   let node: DocumentFragment
   return () => {
@@ -10,16 +10,20 @@ export const template = (str: string): (() => DocumentFragment) => {
       // first render: insert the node directly.
       // this removes it from the template fragment to avoid keeping two copies
       // of the inserted tree in memory, even if the template is used only once.
-      return (node = t.content).cloneNode(true) as DocumentFragment
+      return fragmentToNodes((node = t.content))
     } else {
       // repeated renders: clone from cache. This is more performant and
       // efficient when dealing with big lists where the template is repeated
       // many times.
-      return node.cloneNode(true) as DocumentFragment
+      return fragmentToNodes(node)
     }
   }
 }
 
-export function fragment(): () => Node[] {
+function fragmentToNodes(node: DocumentFragment): ChildNode[] {
+  return Array.from((node.cloneNode(true) as DocumentFragment).childNodes)
+}
+
+export function fragment(): () => ChildNode[] {
   return () => []
 }