mirror of https://github.com/vuejs/core.git
wip(vapor): component hydration
This commit is contained in:
parent
a2415de7bf
commit
e3a33e6092
|
@ -26,7 +26,7 @@ export function render(_ctx) {
|
|||
`;
|
||||
|
||||
exports[`compile > custom directive > component 1`] = `
|
||||
"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, insert as _insert, createIf as _createIf, template as _template } from 'vue';
|
||||
"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, createIf as _createIf, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>")
|
||||
|
||||
export function render(_ctx) {
|
||||
|
@ -38,9 +38,9 @@ export function render(_ctx) {
|
|||
"default": () => {
|
||||
const n0 = _createIf(() => (true), () => {
|
||||
const n3 = t0()
|
||||
_setInsertionState(n3)
|
||||
const n2 = _createComponentWithFallback(_component_Bar)
|
||||
_withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
|
||||
_insert(n2, n3)
|
||||
return n3
|
||||
})
|
||||
return n0
|
||||
|
@ -149,7 +149,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
|
|||
`;
|
||||
|
||||
exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
|
||||
"import { resolveComponent as _resolveComponent, child as _child, createComponentWithFallback as _createComponentWithFallback, prepend as _prepend, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||
"import { resolveComponent as _resolveComponent, child as _child, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||
const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
|
||||
const t1 = _template("<div> </div>")
|
||||
|
||||
|
@ -158,8 +158,8 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
|
|||
const n0 = t0()
|
||||
const n3 = t1()
|
||||
const n2 = _child(n3)
|
||||
_setInsertionState(n3, 0)
|
||||
const n1 = _createComponentWithFallback(_component_Comp)
|
||||
_prepend(n3, n1)
|
||||
_renderEffect(() => {
|
||||
_setText(n2, _toDisplayString(_ctx.bar))
|
||||
_setProp(n3, "id", _ctx.foo)
|
||||
|
|
|
@ -65,20 +65,20 @@ export function render(_ctx) {
|
|||
`;
|
||||
|
||||
exports[`compiler: v-for > nested v-for 1`] = `
|
||||
"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, insert as _insert, template as _template } from 'vue';
|
||||
"import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
|
||||
const t0 = _template("<span> </span>")
|
||||
const t1 = _template("<div></div>", true)
|
||||
|
||||
export function render(_ctx) {
|
||||
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
|
||||
const n5 = t1()
|
||||
_setInsertionState(n5)
|
||||
const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
|
||||
const n4 = t0()
|
||||
const x4 = _child(n4)
|
||||
_renderEffect(() => _setText(x4, _toDisplayString(_for_item1.value+_for_item0.value)))
|
||||
return n4
|
||||
}, null, 1)
|
||||
_insert(n2, n5)
|
||||
return n5
|
||||
})
|
||||
return n0
|
||||
|
|
|
@ -36,14 +36,14 @@ export function render(_ctx) {
|
|||
`;
|
||||
|
||||
exports[`compiler: v-once > on component 1`] = `
|
||||
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, insert as _insert, template as _template } from 'vue';
|
||||
"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>", true)
|
||||
|
||||
export function render(_ctx) {
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n1 = t0()
|
||||
_setInsertionState(n1)
|
||||
const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
|
||||
_insert(n0, n1)
|
||||
return n1
|
||||
}"
|
||||
`;
|
||||
|
|
|
@ -132,10 +132,6 @@ describe('compiler: v-once', () => {
|
|||
id: 0,
|
||||
tag: 'Comp',
|
||||
once: true,
|
||||
},
|
||||
{
|
||||
type: IRNodeTypes.INSERT_NODE,
|
||||
elements: [0],
|
||||
parent: 1,
|
||||
},
|
||||
])
|
||||
|
|
|
@ -51,6 +51,7 @@ export function genCreateComponent(
|
|||
const rawSlots = genRawSlots(slots, context)
|
||||
const [ids, handlers] = processInlineHandlers(props, context)
|
||||
const rawProps = context.withId(() => genRawProps(props, context), ids)
|
||||
|
||||
const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
|
||||
(acc, { name, value }) => {
|
||||
const handler = genEventHandler(context, value, undefined, false)
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { type IREffect, IRNodeTypes, type OperationNode } from '../ir'
|
||||
import {
|
||||
type IREffect,
|
||||
IRNodeTypes,
|
||||
type InsertionStateTypes,
|
||||
type OperationNode,
|
||||
isTypeThatNeedsInsertionState,
|
||||
} from '../ir'
|
||||
import type { CodegenContext } from '../generate'
|
||||
import { genInsertNode, genPrependNode } from './dom'
|
||||
import { genSetDynamicEvents, genSetEvent } from './event'
|
||||
|
@ -14,6 +20,7 @@ import {
|
|||
INDENT_START,
|
||||
NEWLINE,
|
||||
buildCodeFragment,
|
||||
genCall,
|
||||
} from './utils'
|
||||
import { genCreateComponent } from './component'
|
||||
import { genSlotOutlet } from './slotOutlet'
|
||||
|
@ -26,6 +33,9 @@ export function genOperations(
|
|||
): CodeFragment[] {
|
||||
const [frag, push] = buildCodeFragment()
|
||||
for (const operation of opers) {
|
||||
if (isTypeThatNeedsInsertionState(operation) && operation.parent) {
|
||||
push(...genInsertionstate(operation, context))
|
||||
}
|
||||
push(...genOperation(operation, context))
|
||||
}
|
||||
return frag
|
||||
|
@ -134,3 +144,21 @@ export function genEffect(
|
|||
|
||||
return frag
|
||||
}
|
||||
|
||||
function genInsertionstate(
|
||||
operation: InsertionStateTypes,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
return [
|
||||
NEWLINE,
|
||||
...genCall(
|
||||
context.helper('setInsertionState'),
|
||||
`n${operation.parent}`,
|
||||
operation.anchor == null
|
||||
? undefined
|
||||
: operation.anchor === -1 // -1 indicates prepend
|
||||
? `0` // runtime anchor value for prepend
|
||||
: `n${operation.anchor}`,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -76,6 +76,8 @@ export interface IfIRNode extends BaseIRNode {
|
|||
positive: BlockIRNode
|
||||
negative?: BlockIRNode | IfIRNode
|
||||
once?: boolean
|
||||
parent?: number
|
||||
anchor?: number
|
||||
}
|
||||
|
||||
export interface IRFor {
|
||||
|
@ -93,6 +95,8 @@ export interface ForIRNode extends BaseIRNode, IRFor {
|
|||
once: boolean
|
||||
component: boolean
|
||||
onlyChild: boolean
|
||||
parent?: number
|
||||
anchor?: number
|
||||
}
|
||||
|
||||
export interface SetPropIRNode extends BaseIRNode {
|
||||
|
@ -158,6 +162,7 @@ export interface SetTemplateRefIRNode extends BaseIRNode {
|
|||
effect: boolean
|
||||
}
|
||||
|
||||
// TODO remove, no longer needed
|
||||
export interface CreateTextNodeIRNode extends BaseIRNode {
|
||||
type: IRNodeTypes.CREATE_TEXT_NODE
|
||||
id: number
|
||||
|
@ -198,6 +203,8 @@ export interface CreateComponentIRNode extends BaseIRNode {
|
|||
root: boolean
|
||||
once: boolean
|
||||
dynamic?: SimpleExpressionNode
|
||||
parent?: number
|
||||
anchor?: number
|
||||
}
|
||||
|
||||
export interface DeclareOldRefIRNode extends BaseIRNode {
|
||||
|
@ -211,6 +218,8 @@ export interface SlotOutletIRNode extends BaseIRNode {
|
|||
name: SimpleExpressionNode
|
||||
props: IRProps[]
|
||||
fallback?: BlockIRNode
|
||||
parent?: number
|
||||
anchor?: number
|
||||
}
|
||||
|
||||
export interface GetTextChildIRNode extends BaseIRNode {
|
||||
|
@ -288,3 +297,21 @@ export type VaporDirectiveNode = Overwrite<
|
|||
arg: Exclude<DirectiveNode['arg'], CompoundExpressionNode>
|
||||
}
|
||||
>
|
||||
|
||||
export type InsertionStateTypes =
|
||||
| IfIRNode
|
||||
| ForIRNode
|
||||
| SlotOutletIRNode
|
||||
| CreateComponentIRNode
|
||||
|
||||
export function isTypeThatNeedsInsertionState(
|
||||
op: OperationNode,
|
||||
): op is InsertionStateTypes {
|
||||
const type = op.type
|
||||
return (
|
||||
type === IRNodeTypes.CREATE_COMPONENT_NODE ||
|
||||
type === IRNodeTypes.SLOT_OUTLET_NODE ||
|
||||
type === IRNodeTypes.IF ||
|
||||
type === IRNodeTypes.FOR
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,12 @@ import {
|
|||
type TransformContext,
|
||||
transformNode,
|
||||
} from '../transform'
|
||||
import { DynamicFlag, type IRDynamicInfo, IRNodeTypes } from '../ir'
|
||||
import {
|
||||
DynamicFlag,
|
||||
type IRDynamicInfo,
|
||||
IRNodeTypes,
|
||||
isTypeThatNeedsInsertionState as isBlockOperation,
|
||||
} from '../ir'
|
||||
|
||||
export const transformChildren: NodeTransform = (node, context) => {
|
||||
const isFragment =
|
||||
|
@ -66,21 +71,11 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
|
|||
if (prevDynamics.length) {
|
||||
if (hasStaticTemplate) {
|
||||
context.childrenTemplate[index - prevDynamics.length] = `<!>`
|
||||
|
||||
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
|
||||
const anchor = (prevDynamics[0].anchor = context.increaseId())
|
||||
context.registerOperation({
|
||||
type: IRNodeTypes.INSERT_NODE,
|
||||
elements: prevDynamics.map(child => child.id!),
|
||||
parent: context.reference(),
|
||||
anchor,
|
||||
})
|
||||
registerInsertion(prevDynamics, context, anchor)
|
||||
} else {
|
||||
context.registerOperation({
|
||||
type: IRNodeTypes.PREPEND_NODE,
|
||||
elements: prevDynamics.map(child => child.id!),
|
||||
parent: context.reference(),
|
||||
})
|
||||
registerInsertion(prevDynamics, context, -1 /* prepend */)
|
||||
}
|
||||
prevDynamics = []
|
||||
}
|
||||
|
@ -89,10 +84,32 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
|
|||
}
|
||||
|
||||
if (prevDynamics.length) {
|
||||
context.registerOperation({
|
||||
type: IRNodeTypes.INSERT_NODE,
|
||||
elements: prevDynamics.map(child => child.id!),
|
||||
parent: context.reference(),
|
||||
})
|
||||
registerInsertion(prevDynamics, context)
|
||||
}
|
||||
}
|
||||
|
||||
function registerInsertion(
|
||||
dynamics: IRDynamicInfo[],
|
||||
context: TransformContext,
|
||||
anchor?: number,
|
||||
) {
|
||||
for (const child of dynamics) {
|
||||
if (child.template != null) {
|
||||
// template node due to invalid nesting - generate actual insertion
|
||||
context.registerOperation({
|
||||
type: IRNodeTypes.INSERT_NODE,
|
||||
elements: dynamics.map(child => child.id!),
|
||||
parent: context.reference(),
|
||||
anchor,
|
||||
})
|
||||
} else {
|
||||
// block types
|
||||
for (const op of context.block.operation) {
|
||||
if (isBlockOperation(op) && op.id === child.id) {
|
||||
op.parent = context.reference()
|
||||
op.anchor = anchor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
// import { type SSRContext, renderToString } from '@vue/server-renderer'
|
||||
import {
|
||||
child,
|
||||
createComponent,
|
||||
createVaporSSRApp,
|
||||
delegateEvents,
|
||||
next,
|
||||
renderEffect,
|
||||
setClass,
|
||||
setInsertionState,
|
||||
setText,
|
||||
template,
|
||||
} from '../src'
|
||||
|
@ -144,6 +146,117 @@ describe('SSR hydration', () => {
|
|||
)
|
||||
})
|
||||
|
||||
test('basic component', async () => {
|
||||
const t0 = template(' ')
|
||||
const msg = ref('foo')
|
||||
const Comp = {
|
||||
setup() {
|
||||
const n0 = t0() as Text
|
||||
renderEffect(() => setText(n0, toDisplayString(msg.value)))
|
||||
return n0
|
||||
},
|
||||
}
|
||||
|
||||
const t1 = template('<div><span></span></div>', true)
|
||||
const { container } = mountWithHydration(
|
||||
'<div><span></span>foo</div>',
|
||||
() => {
|
||||
const n1 = t1() as Element
|
||||
setInsertionState(n1)
|
||||
createComponent(Comp)
|
||||
return n1
|
||||
},
|
||||
)
|
||||
|
||||
expect(container.innerHTML).toBe(`<div><span></span>foo</div>`)
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe(`<div><span></span>bar</div>`)
|
||||
})
|
||||
|
||||
test('fragment component', async () => {
|
||||
const t0 = template('<div> </div>')
|
||||
const t1 = template(' ')
|
||||
const msg = ref('foo')
|
||||
const Comp = {
|
||||
setup() {
|
||||
const n0 = t0() as Element
|
||||
const n1 = t1() as Text
|
||||
const x0 = child(n0) as Text
|
||||
renderEffect(() => {
|
||||
const _msg = msg.value
|
||||
|
||||
setText(x0, toDisplayString(_msg))
|
||||
setText(n1, toDisplayString(_msg))
|
||||
})
|
||||
return [n0, n1]
|
||||
},
|
||||
}
|
||||
|
||||
const t2 = template('<div><span></span></div>', true)
|
||||
const { container } = mountWithHydration(
|
||||
'<div><span></span><!--[--><div>foo</div>foo<!--]--></div>',
|
||||
() => {
|
||||
const n1 = t2() as Element
|
||||
setInsertionState(n1)
|
||||
createComponent(Comp)
|
||||
return n1
|
||||
},
|
||||
)
|
||||
|
||||
expect(container.innerHTML).toBe(
|
||||
`<div><span></span><!--[--><div>foo</div>foo<!--]--></div>`,
|
||||
)
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe(
|
||||
`<div><span></span><!--[--><div>bar</div>bar<!--]--></div>`,
|
||||
)
|
||||
})
|
||||
|
||||
test('fragment component with prepend', async () => {
|
||||
const t0 = template('<div> </div>')
|
||||
const t1 = template(' ')
|
||||
const msg = ref('foo')
|
||||
const Comp = {
|
||||
setup() {
|
||||
const n0 = t0() as Element
|
||||
const n1 = t1() as Text
|
||||
const x0 = child(n0) as Text
|
||||
renderEffect(() => {
|
||||
const _msg = msg.value
|
||||
|
||||
setText(x0, toDisplayString(_msg))
|
||||
setText(n1, toDisplayString(_msg))
|
||||
})
|
||||
return [n0, n1]
|
||||
},
|
||||
}
|
||||
|
||||
const t2 = template('<div><span></span></div>', true)
|
||||
const { container } = mountWithHydration(
|
||||
'<div><!--[--><div>foo</div>foo<!--]--><span></span></div>',
|
||||
() => {
|
||||
const n1 = t2() as Element
|
||||
setInsertionState(n1, 0)
|
||||
createComponent(Comp)
|
||||
return n1
|
||||
},
|
||||
)
|
||||
|
||||
expect(container.innerHTML).toBe(
|
||||
`<div><!--[--><div>foo</div>foo<!--]--><span></span></div>`,
|
||||
)
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe(
|
||||
`<div><!--[--><div>bar</div>bar<!--]--><span></span></div>`,
|
||||
)
|
||||
})
|
||||
|
||||
// test('element with ref', () => {
|
||||
// const el = ref()
|
||||
// const { vnode, container } = mountWithHydration('<div></div>', () =>
|
||||
|
|
|
@ -58,6 +58,12 @@ import {
|
|||
getSlot,
|
||||
} from './componentSlots'
|
||||
import { hmrReload, hmrRerender } from './hmr'
|
||||
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
||||
import {
|
||||
insertionAnchor,
|
||||
insertionParent,
|
||||
resetInsertionState,
|
||||
} from './insertionState'
|
||||
|
||||
export { currentInstance } from '@vue/runtime-dom'
|
||||
|
||||
|
@ -136,6 +142,10 @@ export function createComponent(
|
|||
currentInstance.appContext) ||
|
||||
emptyContext,
|
||||
): VaporComponentInstance {
|
||||
if (isHydrating) {
|
||||
locateHydrationNode()
|
||||
}
|
||||
|
||||
// vdom interop enabled and component is not an explicit vapor component
|
||||
if (appContext.vapor && !component.__vapor) {
|
||||
return appContext.vapor.vdomMount(component as any, rawProps, rawSlots)
|
||||
|
@ -253,6 +263,11 @@ export function createComponent(
|
|||
|
||||
onScopeDispose(() => unmountComponent(instance), true)
|
||||
|
||||
if (!isHydrating && insertionParent) {
|
||||
insert(instance.block, insertionParent, insertionAnchor)
|
||||
resetInsertionState()
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
import { warn } from '@vue/runtime-dom'
|
||||
import {
|
||||
insertionAnchor,
|
||||
insertionParent,
|
||||
resetInsertionState,
|
||||
setInsertionState,
|
||||
} from '../insertionState'
|
||||
import { child, next } from './node'
|
||||
|
||||
export let isHydrating = false
|
||||
|
@ -10,31 +17,28 @@ export function setCurrentHydrationNode(node: Node | null): void {
|
|||
let isOptimized = false
|
||||
|
||||
export function withHydration(container: ParentNode, fn: () => void): void {
|
||||
adoptHydrationNode = adoptHydrationNodeImpl
|
||||
adoptTemplate = adoptTemplateImpl
|
||||
locateHydrationNode = locateHydrationNodeImpl
|
||||
if (!isOptimized) {
|
||||
// optimize anchor cache lookup
|
||||
const proto = Comment.prototype as any
|
||||
proto.$p = proto.$e = undefined
|
||||
;(Comment.prototype as any).$fs = undefined
|
||||
isOptimized = true
|
||||
}
|
||||
isHydrating = true
|
||||
currentHydrationNode = child(container)
|
||||
setInsertionState(container, 0)
|
||||
const res = fn()
|
||||
resetInsertionState()
|
||||
isHydrating = false
|
||||
currentHydrationNode = null
|
||||
return res
|
||||
}
|
||||
|
||||
export let adoptHydrationNode: (
|
||||
node: Node | null,
|
||||
template?: string,
|
||||
) => Node | null
|
||||
export let adoptTemplate: (node: Node, template: string) => Node | null
|
||||
export let locateHydrationNode: () => void
|
||||
|
||||
type Anchor = Comment & {
|
||||
// previous open anchor
|
||||
$p?: Anchor
|
||||
// matching end anchor
|
||||
$e?: Anchor
|
||||
// cached matching fragment start to avoid repeated traversal
|
||||
// on nested fragments
|
||||
$fs?: Anchor
|
||||
}
|
||||
|
||||
const isComment = (node: Node, data: string): node is Anchor =>
|
||||
|
@ -44,84 +48,82 @@ const isComment = (node: Node, data: string): node is Anchor =>
|
|||
* Locate the first non-fragment-comment node and locate the next node
|
||||
* while handling potential fragments.
|
||||
*/
|
||||
function adoptHydrationNodeImpl(
|
||||
node: Node | null,
|
||||
template?: string,
|
||||
): Node | null {
|
||||
if (!isHydrating || !node) {
|
||||
return node
|
||||
function adoptTemplateImpl(node: Node, template: string): Node | null {
|
||||
if (!(template[0] === '<' && template[1] === '!')) {
|
||||
while (node.nodeType === 8) node = next(node)
|
||||
}
|
||||
|
||||
let adopted: Node | undefined
|
||||
let end: Node | undefined | null
|
||||
|
||||
if (template) {
|
||||
if (template[0] !== '<' && template[1] !== '!') {
|
||||
while (node.nodeType === 8) node = next(node)
|
||||
}
|
||||
adopted = end = node
|
||||
} else if (isComment(node, '[')) {
|
||||
// fragment
|
||||
let start = node
|
||||
let cur: Node = node
|
||||
let fragmentDepth = 1
|
||||
// previously recorded fragment end
|
||||
if (!end && node.$e) {
|
||||
end = node.$e
|
||||
}
|
||||
while (true) {
|
||||
cur = next(cur)
|
||||
if (isComment(cur, '[')) {
|
||||
// previously recorded fragment end
|
||||
if (!end && node.$e) {
|
||||
end = node.$e
|
||||
}
|
||||
fragmentDepth++
|
||||
cur.$p = start
|
||||
start = cur
|
||||
} else if (isComment(cur, ']')) {
|
||||
fragmentDepth--
|
||||
// record fragment end on start node for later traversal
|
||||
start.$e = cur
|
||||
start = start.$p!
|
||||
if (!fragmentDepth) {
|
||||
// fragment end
|
||||
end = cur
|
||||
break
|
||||
}
|
||||
} else if (!adopted) {
|
||||
adopted = cur
|
||||
if (end) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!adopted) {
|
||||
throw new Error('hydration mismatch')
|
||||
}
|
||||
} else {
|
||||
adopted = end = node
|
||||
}
|
||||
|
||||
if (__DEV__ && template) {
|
||||
const type = adopted.nodeType
|
||||
if (__DEV__) {
|
||||
const type = node.nodeType
|
||||
if (
|
||||
(type === 8 && !template.startsWith('<!')) ||
|
||||
(type === 1 &&
|
||||
!template.startsWith(
|
||||
`<` + (adopted as Element).tagName.toLowerCase(),
|
||||
)) ||
|
||||
!template.startsWith(`<` + (node as Element).tagName.toLowerCase())) ||
|
||||
(type === 3 &&
|
||||
template.trim() &&
|
||||
!template.startsWith((adopted as Text).data))
|
||||
!template.startsWith((node as Text).data))
|
||||
) {
|
||||
// TODO recover and provide more info
|
||||
console.error(`adopted: `, adopted)
|
||||
console.error(`template: ${template}`)
|
||||
throw new Error('hydration mismatch!')
|
||||
warn(`adopted: `, node)
|
||||
warn(`template: ${template}`)
|
||||
warn('hydration mismatch!')
|
||||
}
|
||||
}
|
||||
|
||||
currentHydrationNode = next(end!)
|
||||
return adopted
|
||||
currentHydrationNode = next(node)
|
||||
return node
|
||||
}
|
||||
|
||||
function locateHydrationNodeImpl() {
|
||||
if (__DEV__ && !insertionParent) {
|
||||
warn('Hydration error: missing insertion state.')
|
||||
}
|
||||
|
||||
let node: Node | null
|
||||
|
||||
// prepend / firstChild
|
||||
if (insertionAnchor === 0) {
|
||||
node = child(insertionParent!)
|
||||
} else {
|
||||
node = insertionAnchor
|
||||
? insertionAnchor.previousSibling
|
||||
: insertionParent!.lastChild
|
||||
|
||||
if (node && isComment(node, ']')) {
|
||||
// fragment backward search
|
||||
if (node.$fs) {
|
||||
// already cached matching fragment start
|
||||
node = node.$fs
|
||||
} else {
|
||||
let cur: Node | null = node
|
||||
let curFragEnd = node
|
||||
let fragDepth = 0
|
||||
node = null
|
||||
while (cur) {
|
||||
cur = cur.previousSibling
|
||||
if (cur) {
|
||||
if (isComment(cur, '[')) {
|
||||
curFragEnd.$fs = cur
|
||||
if (!fragDepth) {
|
||||
node = cur
|
||||
break
|
||||
} else {
|
||||
fragDepth--
|
||||
}
|
||||
} else if (isComment(cur, ']')) {
|
||||
curFragEnd = cur
|
||||
fragDepth++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentHydrationNode = node
|
||||
|
||||
if (__DEV__ && !currentHydrationNode) {
|
||||
// TODO more info
|
||||
warn('Hydration mismatch in ', insertionParent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
adoptHydrationNode,
|
||||
currentHydrationNode,
|
||||
isHydrating,
|
||||
} from './hydration'
|
||||
import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
|
||||
import { child, createTextNode } from './node'
|
||||
|
||||
let t: HTMLTemplateElement
|
||||
|
@ -16,7 +12,7 @@ export function template(html: string, root?: boolean) {
|
|||
// TODO this should not happen
|
||||
throw new Error('No current hydration node')
|
||||
}
|
||||
return adoptHydrationNode(currentHydrationNode, html)!
|
||||
return adoptTemplate(currentHydrationNode!, html)!
|
||||
}
|
||||
// fast path for text nodes
|
||||
if (html[0] !== '<') {
|
||||
|
|
|
@ -6,6 +6,7 @@ export type { VaporDirective } from './directives/custom'
|
|||
|
||||
// compiler-use only
|
||||
export { insert, prepend, remove, isFragment, VaporFragment } from './block'
|
||||
export { setInsertionState } from './insertionState'
|
||||
export { createComponent, createComponentWithFallback } from './component'
|
||||
export { renderEffect } from './renderEffect'
|
||||
export { createSlot } from './componentSlots'
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { setCurrentHydrationNode } from './dom/hydration'
|
||||
|
||||
export let insertionParent: ParentNode | undefined
|
||||
export let insertionAnchor: Node | 0 | undefined
|
||||
|
||||
/**
|
||||
* This function is called before a block type that requires insertion
|
||||
* (component, slot outlet, if, for) is created. The state is used for actual
|
||||
* insertion on client-side render, and used for node adoption during hydration.
|
||||
*/
|
||||
export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
|
||||
insertionParent = parent
|
||||
insertionAnchor = anchor
|
||||
}
|
||||
|
||||
export function resetInsertionState(): void {
|
||||
insertionParent = insertionAnchor = undefined
|
||||
setCurrentHydrationNode(null)
|
||||
}
|
Loading…
Reference in New Issue