mirror of https://github.com/vuejs/core.git
refactor(runtime-vapor): template fragment (#100)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
93db0a70eb
commit
489f11a1f9
|
@ -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-->')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = []
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => []
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue