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 { defineComponent } from 'vue'
|
||||||
import {
|
import {
|
||||||
|
append,
|
||||||
children,
|
children,
|
||||||
createIf,
|
createIf,
|
||||||
|
fragment,
|
||||||
insert,
|
insert,
|
||||||
nextTick,
|
nextTick,
|
||||||
ref,
|
ref,
|
||||||
|
@ -10,7 +12,6 @@ import {
|
||||||
setText,
|
setText,
|
||||||
template,
|
template,
|
||||||
} from '../src'
|
} from '../src'
|
||||||
import { NOOP } from '@vue/shared'
|
|
||||||
import type { Mock } from 'vitest'
|
import type { Mock } from 'vitest'
|
||||||
|
|
||||||
let host: HTMLElement
|
let host: HTMLElement
|
||||||
|
@ -103,4 +104,65 @@ describe('createIf', () => {
|
||||||
expect(spyIfFn!).toHaveBeenCalledTimes(1)
|
expect(spyIfFn!).toHaveBeenCalledTimes(1)
|
||||||
expect(spyElseFn!).toHaveBeenCalledTimes(2)
|
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', () => {
|
test('create element', () => {
|
||||||
const t = template('<div>')
|
const t = template('<div>')
|
||||||
const root = t()
|
const root = t()
|
||||||
expect(root).toBeInstanceOf(DocumentFragment)
|
expect(root).toBeInstanceOf(Array)
|
||||||
expect(root.childNodes[0]).toBeInstanceOf(HTMLDivElement)
|
expect(root[0]).toBeInstanceOf(HTMLDivElement)
|
||||||
|
|
||||||
const div2 = t()
|
const root2 = t()
|
||||||
expect(div2).toBeInstanceOf(DocumentFragment)
|
expect(root2).toBeInstanceOf(Array)
|
||||||
expect(div2).not.toBe(root)
|
expect(root2).not.toBe(root)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('create fragment', () => {
|
test('create fragment', () => {
|
||||||
|
|
|
@ -5,85 +5,69 @@ export * from './dom/patchProp'
|
||||||
export * from './dom/templateRef'
|
export * from './dom/templateRef'
|
||||||
export * from './dom/on'
|
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) {
|
if (block instanceof Node) {
|
||||||
parent.insertBefore(block, anchor)
|
nodes.push(block)
|
||||||
} else if (isArray(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 {
|
} else {
|
||||||
insert(block.nodes, parent, anchor)
|
normalizeBlock(block).forEach(node => parent.insertBefore(node, anchor))
|
||||||
block.anchor && parent.insertBefore(block.anchor, anchor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepend(parent: ParentBlock, ...blocks: Block[]) {
|
export function prepend(parent: ParentBlock, ...blocks: Block[]) {
|
||||||
const nodes: Node[] = []
|
if (isArray(parent)) {
|
||||||
|
parent.unshift(...blocks)
|
||||||
for (const block of blocks) {
|
} else {
|
||||||
if (block instanceof Node) {
|
parent.prepend(...normalizeBlock(blocks))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function append(parent: ParentBlock, ...blocks: Block[]) {
|
export function append(parent: ParentBlock, ...blocks: Block[]) {
|
||||||
const nodes: Node[] = []
|
if (isArray(parent)) {
|
||||||
|
parent.push(...blocks)
|
||||||
for (const block of blocks) {
|
} else {
|
||||||
if (block instanceof Node) {
|
parent.append(...normalizeBlock(blocks))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function remove(block: Block, parent: ParentNode) {
|
export function remove(block: Block, parent: ParentBlock) {
|
||||||
if (block instanceof DocumentFragment) {
|
if (isArray(parent)) {
|
||||||
remove(Array.from(block.childNodes), parent)
|
const index = parent.indexOf(block)
|
||||||
} else if (block instanceof Node) {
|
if (index > -1) {
|
||||||
parent.removeChild(block)
|
parent.splice(index, 1)
|
||||||
} else if (isArray(block)) {
|
}
|
||||||
for (const child of block) remove(child, parent)
|
|
||||||
} else {
|
} else {
|
||||||
remove(block.nodes, parent)
|
normalizeBlock(block).forEach(node => parent.removeChild(node))
|
||||||
block.anchor && parent.removeChild(block.anchor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Children = Record<number, [ChildNode, Children]>
|
type Children = Record<number, [ChildNode, Children]>
|
||||||
export function children(n: Node): Children {
|
export function children(nodes: ChildNode[]): Children {
|
||||||
const result: Children = {}
|
const result: Children = {}
|
||||||
const array = Array.from(n.childNodes)
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
for (let i = 0; i < array.length; i++) {
|
const n = nodes[i]
|
||||||
const n = array[i]
|
result[i] = [n, children(Array.from(n.childNodes))]
|
||||||
result[i] = [n, children(n)]
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { renderWatch } from './renderWatch'
|
import { renderWatch } from './renderWatch'
|
||||||
import { type BlockFn, type Fragment, fragmentKey } from './render'
|
import { type Block, type Fragment, fragmentKey } from './render'
|
||||||
import { effectScope, onEffectCleanup } from '@vue/reactivity'
|
import { type EffectScope, effectScope } from '@vue/reactivity'
|
||||||
import { createComment, createTextNode, insert, remove } from './dom'
|
import { createComment, createTextNode, insert, remove } from './dom'
|
||||||
|
|
||||||
|
type BlockFn = () => Block
|
||||||
|
|
||||||
export const createIf = (
|
export const createIf = (
|
||||||
condition: () => any,
|
condition: () => any,
|
||||||
b1: BlockFn,
|
b1: BlockFn,
|
||||||
|
@ -11,8 +13,14 @@ export const createIf = (
|
||||||
): Fragment => {
|
): Fragment => {
|
||||||
let branch: BlockFn | undefined
|
let branch: BlockFn | undefined
|
||||||
let parent: ParentNode | undefined | null
|
let parent: ParentNode | undefined | null
|
||||||
|
let block: Block | undefined
|
||||||
|
let scope: EffectScope | undefined
|
||||||
const anchor = __DEV__ ? createComment('if') : createTextNode('')
|
const anchor = __DEV__ ? createComment('if') : createTextNode('')
|
||||||
const fragment: Fragment = { nodes: [], anchor, [fragmentKey]: true }
|
const fragment: Fragment = {
|
||||||
|
nodes: [],
|
||||||
|
anchor,
|
||||||
|
[fragmentKey]: true,
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: SSR
|
// TODO: SSR
|
||||||
// if (isHydrating) {
|
// if (isHydrating) {
|
||||||
|
@ -24,23 +32,16 @@ export const createIf = (
|
||||||
() => !!condition(),
|
() => !!condition(),
|
||||||
value => {
|
value => {
|
||||||
parent ||= anchor.parentNode
|
parent ||= anchor.parentNode
|
||||||
|
if (block) {
|
||||||
|
scope!.stop()
|
||||||
|
remove(block, parent!)
|
||||||
|
}
|
||||||
if ((branch = value ? b1 : b2)) {
|
if ((branch = value ? b1 : b2)) {
|
||||||
let scope = effectScope()
|
scope = effectScope()
|
||||||
let block = scope.run(branch)!
|
fragment.nodes = block = scope.run(branch)!
|
||||||
|
|
||||||
if (block instanceof DocumentFragment) {
|
|
||||||
block = Array.from(block.childNodes)
|
|
||||||
}
|
|
||||||
fragment.nodes = block
|
|
||||||
|
|
||||||
parent && insert(block, parent, anchor)
|
parent && insert(block, parent, anchor)
|
||||||
|
|
||||||
onEffectCleanup(() => {
|
|
||||||
parent ||= anchor.parentNode
|
|
||||||
scope.stop()
|
|
||||||
remove(block, parent!)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
|
scope = block = undefined
|
||||||
fragment.nodes = []
|
fragment.nodes = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,13 +15,12 @@ import { queuePostRenderEffect } from './scheduler'
|
||||||
export const fragmentKey = Symbol('fragment')
|
export const fragmentKey = Symbol('fragment')
|
||||||
|
|
||||||
export type Block = Node | Fragment | Block[]
|
export type Block = Node | Fragment | Block[]
|
||||||
export type ParentBlock = ParentNode | Node[]
|
export type ParentBlock = ParentNode | Block[]
|
||||||
export type Fragment = {
|
export type Fragment = {
|
||||||
nodes: Block
|
nodes: Block
|
||||||
anchor?: Node
|
anchor?: Node
|
||||||
[fragmentKey]: true
|
[fragmentKey]: true
|
||||||
}
|
}
|
||||||
export type BlockFn = (props?: any) => Block
|
|
||||||
|
|
||||||
export function render(
|
export function render(
|
||||||
comp: Component,
|
comp: Component,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const template = (str: string): (() => DocumentFragment) => {
|
export function template(str: string): () => ChildNode[] {
|
||||||
let cached = false
|
let cached = false
|
||||||
let node: DocumentFragment
|
let node: DocumentFragment
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -10,16 +10,20 @@ export const template = (str: string): (() => DocumentFragment) => {
|
||||||
// first render: insert the node directly.
|
// first render: insert the node directly.
|
||||||
// this removes it from the template fragment to avoid keeping two copies
|
// 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.
|
// 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 {
|
} else {
|
||||||
// repeated renders: clone from cache. This is more performant and
|
// repeated renders: clone from cache. This is more performant and
|
||||||
// efficient when dealing with big lists where the template is repeated
|
// efficient when dealing with big lists where the template is repeated
|
||||||
// many times.
|
// 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 () => []
|
return () => []
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue