startMeasure(instance, `mount`)
}
if (instance.bm) invokeArrayFns(instance.bm)
- if (!isHydrating) {
- insert(instance.block, parent, anchor)
- setComponentScopeId(instance)
- }
+ insert(instance.block, parent, anchor)
+ if (!isHydrating) setComponentScopeId(instance)
if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
instance.isMounted = true
if (__DEV__) {
import { warn } from '@vue/runtime-dom'
import {
type ChildItem,
- getHydrationState,
+ incrementIndexOffset,
insertionAnchor,
insertionParent,
resetInsertionState,
// optimize anchor cache lookup
;(Comment.prototype as any).$fe = undefined
;(Node.prototype as any).$pns = undefined
- ;(Node.prototype as any).$idx = undefined
;(Node.prototype as any).$uc = undefined
+ ;(Node.prototype as any).$idx = undefined
;(Node.prototype as any).$children = undefined
+ ;(Node.prototype as any).$idxMap = undefined
+ ;(Node.prototype as any).$prevDynamicCount = undefined
+ ;(Node.prototype as any).$anchorCount = undefined
+ ;(Node.prototype as any).$appendIndex = undefined
+ ;(Node.prototype as any).$indexOffset = undefined
+
isOptimized = true
}
enableHydrationNodeLookup()
isComment(node.previousSibling!, '[')
) {
node = node.parentNode!.insertBefore(createTextNode(' '), node)
+ incrementIndexOffset(node.parentNode!)
break
}
}
function locateHydrationNodeImpl(): void {
let node: Node | null
- if (insertionAnchor !== undefined) {
- const hydrationState = getHydrationState(insertionParent!)!
- const { prevDynamicCount, logicalChildren, appendAnchor } = hydrationState
+ let idxMap: number[] | undefined
+ if (insertionAnchor !== undefined && (idxMap = insertionParent!.$idxMap)) {
+ const {
+ $prevDynamicCount: prevDynamicCount = 0,
+ $appendIndex: appendIndex,
+ $indexOffset: indexOffset = 0,
+ $anchorCount: anchorCount = 0,
+ } = insertionParent!
// prepend
if (insertionAnchor === 0) {
- // use prevDynamicCount as index to locate the hydration node
- node = logicalChildren[prevDynamicCount]
+ // use prevDynamicCount as logical index to locate the hydration node
+ const realIndex = idxMap![prevDynamicCount] + indexOffset
+ node = insertionParent!.childNodes[realIndex]
}
// insert
else if (insertionAnchor instanceof Node) {
// consecutive insert operations locate the correct hydration node.
let { $idx, $uc: usedCount } = insertionAnchor as ChildItem
if (usedCount !== undefined) {
- node = logicalChildren[$idx + usedCount + 1]
+ const realIndex = idxMap![$idx + usedCount + 1] + indexOffset
+ node = insertionParent!.childNodes[realIndex]
usedCount++
} else {
node = insertionAnchor
// first use of this anchor: it doesn't consume the next child
// so we track unique anchor appearances for later offset correction
- hydrationState.uniqueAnchorCount++
+ insertionParent!.$anchorCount = anchorCount + 1
usedCount = 0
}
;(insertionAnchor as ChildItem).$uc = usedCount
}
// append
else {
- if (appendAnchor) {
- node = logicalChildren[(appendAnchor as ChildItem).$idx + 1]
+ let realIndex: number
+ if (appendIndex !== null && appendIndex !== undefined) {
+ realIndex = idxMap![appendIndex + 1] + indexOffset
+ node = insertionParent!.childNodes[realIndex]
} else {
- node =
+ if (insertionAnchor === null) {
// insertionAnchor is null, indicates no previous static nodes
// use the first child as hydration node
- insertionAnchor === null
- ? logicalChildren[0]
- : // insertionAnchor is a number > 0
- // indicates how many static nodes precede the node to append
- // use it as index to locate the hydration node
- logicalChildren[prevDynamicCount + insertionAnchor]
+ realIndex = idxMap![0] + indexOffset
+ node = insertionParent!.childNodes[realIndex]
+ } else {
+ // insertionAnchor is a number > 0
+ // indicates how many static nodes precede the node to append
+ // use it as index to locate the hydration node
+ realIndex = idxMap![prevDynamicCount + insertionAnchor] + indexOffset
+ node = insertionParent!.childNodes[realIndex]
+ }
}
- hydrationState.appendAnchor = node
+ insertionParent!.$appendIndex = (node as ChildItem).$idx
}
- hydrationState.prevDynamicCount++
+ insertionParent!.$prevDynamicCount = prevDynamicCount + 1
} else {
node = currentHydrationNode
if (insertionParent && (!node || node.parentNode !== insertionParent)) {
/* @__NO_SIDE_EFFECTS__ */
-import {
- type ChildItem,
- type InsertionParent,
- getHydrationState,
-} from '../insertionState'
+import type { ChildItem, InsertionParent } from '../insertionState'
export function createElement(tagName: string): HTMLElement {
return document.createElement(tagName)
*/
/* @__NO_SIDE_EFFECTS__ */
export function __nthChild(node: Node, i: number): Node {
- const hydrationState = getHydrationState(node as ParentNode)
- if (hydrationState) {
- const { prevDynamicCount, uniqueAnchorCount, logicalChildren } =
- hydrationState
+ const parent = node as InsertionParent
+ if (parent.$idxMap) {
+ const {
+ $prevDynamicCount: prevDynamicCount = 0,
+ $anchorCount: anchorCount = 0,
+ $idxMap: idxMap,
+ $indexOffset: indexOffset = 0,
+ } = parent
// prevDynamicCount tracks how many dynamic nodes have been processed
// so far (prepend/insert/append).
// For anchor-based insert, the first time an anchor is used we adopt the
- // anchor node itself and do NOT consume the next child in `logicalChildren`,
+ // anchor node itself and do NOT consume the next child in `idxMap`,
// yet prevDynamicCount is still incremented. This overcounts the base
// offset by 1 per unique anchor that has appeared.
- // uniqueAnchorCount equals the number of unique anchors seen, so we
+ // anchorCount equals the number of unique anchors seen, so we
// subtract it to neutralize those "first-use doesn't consume" cases:
- // base = prevDynamicCount - uniqueAnchorCount
- // Then index from this base: logicalChildren[base + i].
- return logicalChildren[prevDynamicCount - uniqueAnchorCount + i]
+ // base = prevDynamicCount - anchorCount
+ // Then index from this base: idxMap[base + i] + indexOffset.
+ const logicalIndex = prevDynamicCount - anchorCount + i
+ const realIndex = idxMap[logicalIndex] + indexOffset
+ return node.childNodes[realIndex]
}
return node.childNodes[i]
}
*/
/* @__NO_SIDE_EFFECTS__ */
export function __next(node: Node): Node {
- const hydrationState = getHydrationState(node.parentNode!)
- if (hydrationState) {
- const { logicalChildren } = hydrationState
+ const parent = node.parentNode! as InsertionParent
+ if (parent.$idxMap) {
+ const { $idxMap: idxMap, $indexOffset: indexOffset = 0 } = parent
const { $idx, $uc: usedCount = 0 } = node as ChildItem
- return logicalChildren[$idx + usedCount + 1]
+ const logicalIndex = $idx + usedCount + 1
+ const realIndex = idxMap[logicalIndex] + indexOffset
+ return node.parentNode!.childNodes[realIndex]
}
return node.nextSibling!
}
proto.$transition = undefined
proto.$key = undefined
proto.$evtclick = undefined
+ proto.$children = undefined
+ proto.$idx = undefined
proto.$root = false
proto.$html =
proto.$txt =
let t: HTMLTemplateElement
+export let currentTemplateFn: (Function & { $idxMap?: number[] }) | undefined =
+ undefined
+
+export function resetTemplateFn(): void {
+ currentTemplateFn = undefined
+}
+
/*! #__NO_SIDE_EFFECTS__ */
-export function template(html: string, root?: boolean) {
+export function template(
+ html: string,
+ root?: boolean,
+): () => Node & { $root?: true } {
let node: Node
- return (): Node & { $root?: true } => {
+ const fn = () => {
if (isHydrating) {
+ currentTemplateFn = fn
if (__DEV__ && !currentHydrationNode) {
// TODO this should not happen
throw new Error('No current hydration node')
if (root) (ret as any).$root = true
return ret
}
+ return fn
}
} from './components/Transition'
import { type VaporComponentInstance, isVaporComponent } from './component'
import { isArray } from '@vue/shared'
+import { incrementIndexOffset } from './insertionState'
export class VaporFragment<T extends Block = Block>
implements TransitionOptions
(this.anchor = createComment(this.anchorLabel!)),
nextSibling,
)
+ // increment index offset since we dynamically inserted a comment node
+ incrementIndexOffset(parentNode!)
advanceHydrationNode(this.anchor)
}
}
import { isComment, isHydrating } from './dom/hydration'
+import { currentTemplateFn, resetTemplateFn } from './dom/template'
export type ChildItem = ChildNode & {
$idx: number
// used count as an anchor
$uc?: number
}
-export type InsertionParent = ParentNode & { $children?: ChildItem[] }
-type HydrationState = {
- // static nodes and the start anchors of fragments
- logicalChildren: ChildItem[]
+
+export type InsertionParent = ParentNode & {
+ $children?: ChildItem[]
+ /**
+ * hydration-specific properties
+ */
+ // mapping from logical index to real index in childNodes
+ $idxMap?: number[]
// hydrated dynamic children count so far
- prevDynamicCount: number
+ $prevDynamicCount?: number
// number of unique insertion anchors that have appeared
- uniqueAnchorCount: number
- // current append anchor
- appendAnchor: Node | null
+ $anchorCount?: number
+ // last append index
+ $appendIndex?: number | null
+ // number of dynamically inserted nodes (e.g., comment anchors)
+ $indexOffset?: number
}
-
-const hydrationStateCache = new WeakMap<ParentNode, HydrationState>()
export let insertionParent: InsertionParent | undefined
export let insertionAnchor: Node | 0 | undefined | null
if (isHydrating) {
insertionAnchor = anchor as Node
initializeHydrationState(parent)
+ resetTemplateFn()
} else {
// special handling append anchor value to null
insertionAnchor =
}
}
-function initializeHydrationState(parent: ParentNode) {
- if (!hydrationStateCache.has(parent)) {
+function initializeHydrationState(parent: InsertionParent) {
+ if (!parent.$idxMap) {
const childNodes = parent.childNodes
const len = childNodes.length
- // fast path for single child case. No need to build logicalChildren
+ // fast path for single child case. use first child as hydration node
+ // no need to build logical index map
if (
len === 1 ||
(len === 3 &&
return
}
- const logicalChildren = new Array(len) as ChildItem[]
- // Build logical children:
- // - static node: keep the node as a child
- // - fragment: keep only the start anchor ('<!--[-->') as a child
- let index = 0
- for (let i = 0; i < len; i++) {
- const n = childNodes[i] as ChildItem
- n.$idx = index
- if (n.nodeType === 8) {
- const data = (n as any as Comment).data
- // vdom fragment
- if (data === '[') {
- logicalChildren[index++] = n
- // find matching end anchor, accounting for nested fragments
- let depth = 1
- let j = i + 1
- for (; j < len; j++) {
- const c = childNodes[j] as Comment
- if (c.nodeType === 8) {
- const d = c.data
- if (d === '[') depth++
- else if (d === ']') {
- depth--
- if (depth === 0) break
- }
+ if (currentTemplateFn) {
+ if (currentTemplateFn.$idxMap) {
+ const idxMap = (parent.$idxMap = currentTemplateFn.$idxMap)
+ // set $idx to childNodes
+ for (let i = 0; i < idxMap.length; i++) {
+ ;(childNodes[idxMap[i]] as ChildItem).$idx = i
+ }
+ } else {
+ parent.$idxMap = currentTemplateFn.$idxMap = buildLogicalIndexMap(
+ len,
+ childNodes,
+ )
+ }
+ } else {
+ parent.$idxMap = buildLogicalIndexMap(len, childNodes)
+ }
+ parent.$prevDynamicCount = 0
+ parent.$anchorCount = 0
+ parent.$appendIndex = null
+ parent.$indexOffset = 0
+ }
+}
+
+function buildLogicalIndexMap(len: number, childNodes: NodeListOf<ChildNode>) {
+ const idxMap = new Array() as number[]
+ // Build logical index map:
+ // - static node: map logical index to real index
+ // - fragment: map logical index to start anchor's real index
+ let logicalIndex = 0
+ for (let i = 0; i < len; i++) {
+ const n = childNodes[i] as ChildItem
+ n.$idx = logicalIndex
+ if (n.nodeType === 8) {
+ const data = (n as any as Comment).data
+ // vdom fragment
+ if (data === '[') {
+ idxMap[logicalIndex++] = i
+ // find matching end anchor, accounting for nested fragments
+ let depth = 1
+ let j = i + 1
+ for (; j < len; j++) {
+ const c = childNodes[j] as Comment
+ if (c.nodeType === 8) {
+ const d = c.data
+ if (d === '[') depth++
+ else if (d === ']') {
+ depth--
+ if (depth === 0) break
}
}
- // jump i to the end anchor
- i = j
- continue
}
+ // jump i to the end anchor
+ i = j
+ continue
}
- logicalChildren[index++] = n
}
- logicalChildren.length = index
- hydrationStateCache.set(parent, {
- logicalChildren,
- prevDynamicCount: 0,
- uniqueAnchorCount: 0,
- appendAnchor: null,
- })
+ idxMap[logicalIndex++] = i
}
+ return idxMap
}
function cacheTemplateChildren(parent: InsertionParent) {
insertionParent = insertionAnchor = undefined
}
-export function getHydrationState(
- parent: ParentNode,
-): HydrationState | undefined {
- return hydrationStateCache.get(parent)
+export function incrementIndexOffset(parent: InsertionParent): void {
+ if (parent.$indexOffset !== undefined) {
+ parent.$indexOffset++
+ }
}