Skip to content

Commit 489f11a

Browse files
LittleSoundsxzz
andauthored
refactor(runtime-vapor): template fragment (#100)
Co-authored-by: 三咲智子 Kevin Deng <[email protected]>
1 parent 93db0a7 commit 489f11a

File tree

6 files changed

+136
-86
lines changed

6 files changed

+136
-86
lines changed

packages/runtime-vapor/__tests__/if.spec.ts

+63-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { defineComponent } from 'vue'
22
import {
3+
append,
34
children,
45
createIf,
6+
fragment,
57
insert,
68
nextTick,
79
ref,
@@ -10,7 +12,6 @@ import {
1012
setText,
1113
template,
1214
} from '../src'
13-
import { NOOP } from '@vue/shared'
1415
import type { Mock } from 'vitest'
1516

1617
let host: HTMLElement
@@ -103,4 +104,65 @@ describe('createIf', () => {
103104
expect(spyIfFn!).toHaveBeenCalledTimes(1)
104105
expect(spyElseFn!).toHaveBeenCalledTimes(2)
105106
})
107+
108+
test('should handle nested template', async () => {
109+
// mock this template:
110+
// <template v-if="ok1">
111+
// Hello <template v-if="ok2">Vapor</template>
112+
// </template>
113+
114+
const ok1 = ref(true)
115+
const ok2 = ref(true)
116+
117+
const t0 = template('Vapor')
118+
const t1 = template('Hello ')
119+
const t2 = fragment()
120+
render(
121+
defineComponent({
122+
setup() {
123+
// render
124+
return (() => {
125+
const n0 = t2()
126+
append(
127+
n0,
128+
createIf(
129+
() => ok1.value,
130+
() => {
131+
const n2 = t1()
132+
append(
133+
n2,
134+
createIf(
135+
() => ok2.value,
136+
() => t0(),
137+
),
138+
)
139+
return n2
140+
},
141+
),
142+
)
143+
return n0
144+
})()
145+
},
146+
}) as any,
147+
{},
148+
'#host',
149+
)
150+
expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
151+
152+
ok1.value = false
153+
await nextTick()
154+
expect(host.innerHTML).toBe('<!--if-->')
155+
156+
ok1.value = true
157+
await nextTick()
158+
expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
159+
160+
ok2.value = false
161+
await nextTick()
162+
expect(host.innerHTML).toBe('Hello <!--if--><!--if-->')
163+
164+
ok1.value = false
165+
await nextTick()
166+
expect(host.innerHTML).toBe('<!--if-->')
167+
})
106168
})

packages/runtime-vapor/__tests__/template.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ describe('api: template', () => {
44
test('create element', () => {
55
const t = template('<div>')
66
const root = t()
7-
expect(root).toBeInstanceOf(DocumentFragment)
8-
expect(root.childNodes[0]).toBeInstanceOf(HTMLDivElement)
7+
expect(root).toBeInstanceOf(Array)
8+
expect(root[0]).toBeInstanceOf(HTMLDivElement)
99

10-
const div2 = t()
11-
expect(div2).toBeInstanceOf(DocumentFragment)
12-
expect(div2).not.toBe(root)
10+
const root2 = t()
11+
expect(root2).toBeInstanceOf(Array)
12+
expect(root2).not.toBe(root)
1313
})
1414

1515
test('create fragment', () => {

packages/runtime-vapor/src/dom.ts

+41-57
Original file line numberDiff line numberDiff line change
@@ -5,85 +5,69 @@ export * from './dom/patchProp'
55
export * from './dom/templateRef'
66
export * from './dom/on'
77

8-
export function insert(block: Block, parent: Node, anchor: Node | null = null) {
8+
function normalizeBlock(block: Block): Node[] {
9+
const nodes: Node[] = []
910
if (block instanceof Node) {
10-
parent.insertBefore(block, anchor)
11+
nodes.push(block)
1112
} else if (isArray(block)) {
12-
for (const child of block) insert(child, parent, anchor)
13-
} else {
14-
insert(block.nodes, parent, anchor)
15-
block.anchor && parent.insertBefore(block.anchor, anchor)
13+
block.forEach(child => nodes.push(...normalizeBlock(child)))
14+
} else if (block) {
15+
nodes.push(...normalizeBlock(block.nodes))
16+
block.anchor && nodes.push(block.anchor)
1617
}
18+
return nodes
1719
}
1820

19-
export function prepend(parent: ParentBlock, ...blocks: Block[]) {
20-
const nodes: Node[] = []
21-
22-
for (const block of blocks) {
23-
if (block instanceof Node) {
24-
nodes.push(block)
25-
} else if (isArray(block)) {
26-
prepend(parent, ...block)
21+
export function insert(
22+
block: Block,
23+
parent: ParentBlock,
24+
anchor: Node | null = null,
25+
) {
26+
if (isArray(parent)) {
27+
const index = anchor ? parent.indexOf(anchor) : -1
28+
if (index > -1) {
29+
parent.splice(index, 0, block)
2730
} else {
28-
prepend(parent, block.nodes)
29-
block.anchor && prepend(parent, block.anchor)
31+
parent.push(block)
3032
}
33+
} else {
34+
normalizeBlock(block).forEach(node => parent.insertBefore(node, anchor))
3135
}
36+
}
3237

33-
if (!nodes.length) return
34-
35-
if (parent instanceof Node) {
36-
// TODO use insertBefore for better performance https://jsbench.me/rolpg250hh/1
37-
parent.prepend(...nodes)
38-
} else if (isArray(parent)) {
39-
parent.unshift(...nodes)
38+
export function prepend(parent: ParentBlock, ...blocks: Block[]) {
39+
if (isArray(parent)) {
40+
parent.unshift(...blocks)
41+
} else {
42+
parent.prepend(...normalizeBlock(blocks))
4043
}
4144
}
4245

4346
export function append(parent: ParentBlock, ...blocks: Block[]) {
44-
const nodes: Node[] = []
45-
46-
for (const block of blocks) {
47-
if (block instanceof Node) {
48-
nodes.push(block)
49-
} else if (isArray(block)) {
50-
append(parent, ...block)
51-
} else {
52-
append(parent, block.nodes)
53-
block.anchor && append(parent, block.anchor)
54-
}
55-
}
56-
57-
if (!nodes.length) return
58-
59-
if (parent instanceof Node) {
60-
// TODO use insertBefore for better performance
61-
parent.append(...nodes)
62-
} else if (isArray(parent)) {
63-
parent.push(...nodes)
47+
if (isArray(parent)) {
48+
parent.push(...blocks)
49+
} else {
50+
parent.append(...normalizeBlock(blocks))
6451
}
6552
}
6653

67-
export function remove(block: Block, parent: ParentNode) {
68-
if (block instanceof DocumentFragment) {
69-
remove(Array.from(block.childNodes), parent)
70-
} else if (block instanceof Node) {
71-
parent.removeChild(block)
72-
} else if (isArray(block)) {
73-
for (const child of block) remove(child, parent)
54+
export function remove(block: Block, parent: ParentBlock) {
55+
if (isArray(parent)) {
56+
const index = parent.indexOf(block)
57+
if (index > -1) {
58+
parent.splice(index, 1)
59+
}
7460
} else {
75-
remove(block.nodes, parent)
76-
block.anchor && parent.removeChild(block.anchor)
61+
normalizeBlock(block).forEach(node => parent.removeChild(node))
7762
}
7863
}
7964

8065
type Children = Record<number, [ChildNode, Children]>
81-
export function children(n: Node): Children {
66+
export function children(nodes: ChildNode[]): Children {
8267
const result: Children = {}
83-
const array = Array.from(n.childNodes)
84-
for (let i = 0; i < array.length; i++) {
85-
const n = array[i]
86-
result[i] = [n, children(n)]
68+
for (let i = 0; i < nodes.length; i++) {
69+
const n = nodes[i]
70+
result[i] = [n, children(Array.from(n.childNodes))]
8771
}
8872
return result
8973
}

packages/runtime-vapor/src/if.ts

+18-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { renderWatch } from './renderWatch'
2-
import { type BlockFn, type Fragment, fragmentKey } from './render'
3-
import { effectScope, onEffectCleanup } from '@vue/reactivity'
2+
import { type Block, type Fragment, fragmentKey } from './render'
3+
import { type EffectScope, effectScope } from '@vue/reactivity'
44
import { createComment, createTextNode, insert, remove } from './dom'
55

6+
type BlockFn = () => Block
7+
68
export const createIf = (
79
condition: () => any,
810
b1: BlockFn,
@@ -11,8 +13,14 @@ export const createIf = (
1113
): Fragment => {
1214
let branch: BlockFn | undefined
1315
let parent: ParentNode | undefined | null
16+
let block: Block | undefined
17+
let scope: EffectScope | undefined
1418
const anchor = __DEV__ ? createComment('if') : createTextNode('')
15-
const fragment: Fragment = { nodes: [], anchor, [fragmentKey]: true }
19+
const fragment: Fragment = {
20+
nodes: [],
21+
anchor,
22+
[fragmentKey]: true,
23+
}
1624

1725
// TODO: SSR
1826
// if (isHydrating) {
@@ -24,23 +32,16 @@ export const createIf = (
2432
() => !!condition(),
2533
value => {
2634
parent ||= anchor.parentNode
35+
if (block) {
36+
scope!.stop()
37+
remove(block, parent!)
38+
}
2739
if ((branch = value ? b1 : b2)) {
28-
let scope = effectScope()
29-
let block = scope.run(branch)!
30-
31-
if (block instanceof DocumentFragment) {
32-
block = Array.from(block.childNodes)
33-
}
34-
fragment.nodes = block
35-
40+
scope = effectScope()
41+
fragment.nodes = block = scope.run(branch)!
3642
parent && insert(block, parent, anchor)
37-
38-
onEffectCleanup(() => {
39-
parent ||= anchor.parentNode
40-
scope.stop()
41-
remove(block, parent!)
42-
})
4343
} else {
44+
scope = block = undefined
4445
fragment.nodes = []
4546
}
4647
},

packages/runtime-vapor/src/render.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ import { queuePostRenderEffect } from './scheduler'
1515
export const fragmentKey = Symbol('fragment')
1616

1717
export type Block = Node | Fragment | Block[]
18-
export type ParentBlock = ParentNode | Node[]
18+
export type ParentBlock = ParentNode | Block[]
1919
export type Fragment = {
2020
nodes: Block
2121
anchor?: Node
2222
[fragmentKey]: true
2323
}
24-
export type BlockFn = (props?: any) => Block
2524

2625
export function render(
2726
comp: Component,
+8-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const template = (str: string): (() => DocumentFragment) => {
1+
export function template(str: string): () => ChildNode[] {
22
let cached = false
33
let node: DocumentFragment
44
return () => {
@@ -10,16 +10,20 @@ export const template = (str: string): (() => DocumentFragment) => {
1010
// first render: insert the node directly.
1111
// this removes it from the template fragment to avoid keeping two copies
1212
// of the inserted tree in memory, even if the template is used only once.
13-
return (node = t.content).cloneNode(true) as DocumentFragment
13+
return fragmentToNodes((node = t.content))
1414
} else {
1515
// repeated renders: clone from cache. This is more performant and
1616
// efficient when dealing with big lists where the template is repeated
1717
// many times.
18-
return node.cloneNode(true) as DocumentFragment
18+
return fragmentToNodes(node)
1919
}
2020
}
2121
}
2222

23-
export function fragment(): () => Node[] {
23+
function fragmentToNodes(node: DocumentFragment): ChildNode[] {
24+
return Array.from((node.cloneNode(true) as DocumentFragment).childNodes)
25+
}
26+
27+
export function fragment(): () => ChildNode[] {
2428
return () => []
2529
}

0 commit comments

Comments
 (0)