diff --git a/packages/runtime-vapor/__tests__/if.spec.ts b/packages/runtime-vapor/__tests__/if.spec.ts index 0910a6a92..ee4b87907 100644 --- a/packages/runtime-vapor/__tests__/if.spec.ts +++ b/packages/runtime-vapor/__tests__/if.spec.ts @@ -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 @@ -37,9 +38,7 @@ describe('createIf', () => { let spyIfFn: Mock let spyElseFn: Mock - - let add = NOOP - let reset = NOOP + const count = ref(0) // templates can be reused through caching. const t0 = template('
') @@ -48,10 +47,6 @@ describe('createIf', () => { const component = defineComponent({ setup() { - const counter = ref(0) - add = () => counter.value++ - reset = () => (counter.value = 0) - // render return (() => { const n0 = t0() @@ -61,7 +56,7 @@ describe('createIf', () => { insert( createIf( - () => counter.value, + () => count.value, // v-if (spyIfFn ||= vi.fn(() => { const n2 = t1() @@ -69,7 +64,7 @@ describe('createIf', () => { 0: [n3], } = children(n2) renderEffect(() => { - setText(n3, counter.value) + setText(n3, count.value) }) return n2 })), @@ -91,22 +86,83 @@ describe('createIf', () => { expect(spyIfFn!).toHaveBeenCalledTimes(0) expect(spyElseFn!).toHaveBeenCalledTimes(1) - add() + count.value++ await nextTick() expect(host.innerHTML).toBe('

1

') expect(spyIfFn!).toHaveBeenCalledTimes(1) expect(spyElseFn!).toHaveBeenCalledTimes(1) - add() + count.value++ await nextTick() expect(host.innerHTML).toBe('

2

') expect(spyIfFn!).toHaveBeenCalledTimes(1) expect(spyElseFn!).toHaveBeenCalledTimes(1) - reset() + count.value = 0 await nextTick() expect(host.innerHTML).toBe('

zero

') expect(spyIfFn!).toHaveBeenCalledTimes(1) expect(spyElseFn!).toHaveBeenCalledTimes(2) }) + + test('should handle nested template', async () => { + // mock this 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') + + ok1.value = false + await nextTick() + expect(host.innerHTML).toBe('') + + ok1.value = true + await nextTick() + expect(host.innerHTML).toBe('Hello Vapor') + + ok2.value = false + await nextTick() + expect(host.innerHTML).toBe('Hello ') + + ok1.value = false + await nextTick() + expect(host.innerHTML).toBe('') + }) }) diff --git a/packages/runtime-vapor/__tests__/template.spec.ts b/packages/runtime-vapor/__tests__/template.spec.ts index 16ac33833..84c45783d 100644 --- a/packages/runtime-vapor/__tests__/template.spec.ts +++ b/packages/runtime-vapor/__tests__/template.spec.ts @@ -4,12 +4,12 @@ describe('api: template', () => { test('create element', () => { const t = template('
') 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', () => { diff --git a/packages/runtime-vapor/src/dom.ts b/packages/runtime-vapor/src/dom.ts index 4fe2f0635..eedb73722 100644 --- a/packages/runtime-vapor/src/dom.ts +++ b/packages/runtime-vapor/src/dom.ts @@ -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 -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 } diff --git a/packages/runtime-vapor/src/if.ts b/packages/runtime-vapor/src/if.ts index a5072ed72..e2b50f266 100644 --- a/packages/runtime-vapor/src/if.ts +++ b/packages/runtime-vapor/src/if.ts @@ -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 = [] } }, diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 8e81cab21..8bae59953 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -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, diff --git a/packages/runtime-vapor/src/template.ts b/packages/runtime-vapor/src/template.ts index 8b505a6ec..17ab8e5a4 100644 --- a/packages/runtime-vapor/src/template.ts +++ b/packages/runtime-vapor/src/template.ts @@ -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 () => [] }