refactor(runtime-vapor): template fragment (#100)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Rizumu Ayaka 2024-01-30 04:15:52 +08:00 committed by GitHub
parent 93db0a70eb
commit 489f11a1f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 138 additions and 88 deletions

View 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-->')
})
})

View 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', () => {

View 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)
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 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 {
parent.push(block)
}
} else {
insert(block.nodes, parent, anchor)
block.anchor && parent.insertBefore(block.anchor, anchor)
normalizeBlock(block).forEach(node => parent.insertBefore(node, anchor))
}
}
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)
} else {
prepend(parent, block.nodes)
block.anchor && prepend(parent, block.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)
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
}

View 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 = []
}
},

View 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,

View 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 () => []
}