vue3-core/packages/runtime-vapor/src/block.ts

196 lines
4.9 KiB
TypeScript

import { isArray } from '@vue/shared'
import {
type VaporComponentInstance,
isVaporComponent,
mountComponent,
unmountComponent,
} from './component'
import { createComment, createTextNode } from './dom/node'
import { EffectScope, setActiveSub } from '@vue/reactivity'
import { isHydrating } from './dom/hydration'
export type Block =
| Node
| VaporFragment
| DynamicFragment
| VaporComponentInstance
| Block[]
export type BlockFn = (...args: any[]) => Block
export class VaporFragment {
nodes: Block
anchor?: Node
insert?: (parent: ParentNode, anchor: Node | null) => void
remove?: (parent?: ParentNode) => void
constructor(nodes: Block) {
this.nodes = nodes
}
}
export class DynamicFragment extends VaporFragment {
anchor: Node
scope: EffectScope | undefined
current?: BlockFn
fallback?: BlockFn
constructor(anchorLabel?: string) {
super([])
this.anchor =
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
}
update(render?: BlockFn, key: any = render): void {
if (key === this.current) {
return
}
this.current = key
const prevSub = setActiveSub()
const parent = this.anchor.parentNode
// teardown previous branch
if (this.scope) {
this.scope.stop()
parent && remove(this.nodes, parent)
}
if (render) {
this.scope = new EffectScope()
this.nodes = this.scope.run(render) || []
if (parent) insert(this.nodes, parent, this.anchor)
} else {
this.scope = undefined
this.nodes = []
}
if (this.fallback && !isValidBlock(this.nodes)) {
parent && remove(this.nodes, parent)
// if current nodes is a DynamicFragment, call its update with the fallback
// to handle nested dynamic fragment
if (this.nodes instanceof DynamicFragment) {
this.nodes.update(this.fallback)
} else {
this.nodes =
(this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
[]
}
parent && insert(this.nodes, parent, this.anchor)
}
setActiveSub(prevSub)
}
}
export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
return val instanceof VaporFragment
}
export function isBlock(val: NonNullable<unknown>): val is Block {
return (
val instanceof Node ||
isArray(val) ||
isVaporComponent(val) ||
isFragment(val)
)
}
export function isValidBlock(block: Block): boolean {
if (block instanceof Node) {
return !(block instanceof Comment)
} else if (isVaporComponent(block)) {
return isValidBlock(block.block)
} else if (isArray(block)) {
return block.length > 0 && block.every(isValidBlock)
} else {
// fragment
return isValidBlock(block.nodes)
}
}
export function insert(
block: Block,
parent: ParentNode,
anchor: Node | null | 0 = null, // 0 means prepend
): void {
anchor = anchor === 0 ? parent.firstChild : anchor
if (block instanceof Node) {
if (!isHydrating) {
parent.insertBefore(block, anchor)
}
} else if (isVaporComponent(block)) {
if (block.isMounted) {
insert(block.block!, parent, anchor)
} else {
mountComponent(block, parent, anchor)
}
} else if (isArray(block)) {
for (const b of block) {
insert(b, parent, anchor)
}
} else {
// fragment
if (block.insert) {
// TODO handle hydration for vdom interop
block.insert(parent, anchor)
} else {
insert(block.nodes, parent, anchor)
}
if (block.anchor) insert(block.anchor, parent, anchor)
}
}
export type InsertFn = typeof insert
export function prepend(parent: ParentNode, ...blocks: Block[]): void {
let i = blocks.length
while (i--) insert(blocks[i], parent, 0)
}
export function remove(block: Block, parent?: ParentNode): void {
if (block instanceof Node) {
parent && parent.removeChild(block)
} else if (isVaporComponent(block)) {
unmountComponent(block, parent)
} else if (isArray(block)) {
for (let i = 0; i < block.length; i++) {
remove(block[i], parent)
}
} else {
// fragment
if (block.remove) {
block.remove(parent)
} else {
remove(block.nodes, parent)
}
if (block.anchor) remove(block.anchor, parent)
if ((block as DynamicFragment).scope) {
;(block as DynamicFragment).scope!.stop()
}
}
}
/**
* dev / test only
*/
export function normalizeBlock(block: Block): Node[] {
if (!__DEV__ && !__TEST__) {
throw new Error(
'normalizeBlock should not be used in production code paths',
)
}
const nodes: Node[] = []
if (block instanceof Node) {
nodes.push(block)
} else if (isArray(block)) {
block.forEach(child => nodes.push(...normalizeBlock(child)))
} else if (isVaporComponent(block)) {
nodes.push(...normalizeBlock(block.block!))
} else {
nodes.push(...normalizeBlock(block.nodes))
block.anchor && nodes.push(block.anchor)
}
return nodes
}