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`] = `
|
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>")
|
const t0 = _template("<div></div>")
|
||||||
|
|
||||||
export function render(_ctx) {
|
export function render(_ctx) {
|
||||||
|
@ -38,9 +38,9 @@ export function render(_ctx) {
|
||||||
"default": () => {
|
"default": () => {
|
||||||
const n0 = _createIf(() => (true), () => {
|
const n0 = _createIf(() => (true), () => {
|
||||||
const n3 = t0()
|
const n3 = t0()
|
||||||
|
_setInsertionState(n3)
|
||||||
const n2 = _createComponentWithFallback(_component_Bar)
|
const n2 = _createComponentWithFallback(_component_Bar)
|
||||||
_withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
|
_withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
|
||||||
_insert(n2, n3)
|
|
||||||
return n3
|
return n3
|
||||||
})
|
})
|
||||||
return n0
|
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`] = `
|
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 t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
|
||||||
const t1 = _template("<div> </div>")
|
const t1 = _template("<div> </div>")
|
||||||
|
|
||||||
|
@ -158,8 +158,8 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
|
||||||
const n0 = t0()
|
const n0 = t0()
|
||||||
const n3 = t1()
|
const n3 = t1()
|
||||||
const n2 = _child(n3)
|
const n2 = _child(n3)
|
||||||
|
_setInsertionState(n3, 0)
|
||||||
const n1 = _createComponentWithFallback(_component_Comp)
|
const n1 = _createComponentWithFallback(_component_Comp)
|
||||||
_prepend(n3, n1)
|
|
||||||
_renderEffect(() => {
|
_renderEffect(() => {
|
||||||
_setText(n2, _toDisplayString(_ctx.bar))
|
_setText(n2, _toDisplayString(_ctx.bar))
|
||||||
_setProp(n3, "id", _ctx.foo)
|
_setProp(n3, "id", _ctx.foo)
|
||||||
|
|
|
@ -65,20 +65,20 @@ export function render(_ctx) {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`compiler: v-for > nested v-for 1`] = `
|
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 t0 = _template("<span> </span>")
|
||||||
const t1 = _template("<div></div>", true)
|
const t1 = _template("<div></div>", true)
|
||||||
|
|
||||||
export function render(_ctx) {
|
export function render(_ctx) {
|
||||||
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
|
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
|
||||||
const n5 = t1()
|
const n5 = t1()
|
||||||
|
_setInsertionState(n5)
|
||||||
const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
|
const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
|
||||||
const n4 = t0()
|
const n4 = t0()
|
||||||
const x4 = _child(n4)
|
const x4 = _child(n4)
|
||||||
_renderEffect(() => _setText(x4, _toDisplayString(_for_item1.value+_for_item0.value)))
|
_renderEffect(() => _setText(x4, _toDisplayString(_for_item1.value+_for_item0.value)))
|
||||||
return n4
|
return n4
|
||||||
}, null, 1)
|
}, null, 1)
|
||||||
_insert(n2, n5)
|
|
||||||
return n5
|
return n5
|
||||||
})
|
})
|
||||||
return n0
|
return n0
|
||||||
|
|
|
@ -36,14 +36,14 @@ export function render(_ctx) {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`compiler: v-once > on component 1`] = `
|
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)
|
const t0 = _template("<div></div>", true)
|
||||||
|
|
||||||
export function render(_ctx) {
|
export function render(_ctx) {
|
||||||
const _component_Comp = _resolveComponent("Comp")
|
const _component_Comp = _resolveComponent("Comp")
|
||||||
const n1 = t0()
|
const n1 = t0()
|
||||||
|
_setInsertionState(n1)
|
||||||
const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
|
const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
|
||||||
_insert(n0, n1)
|
|
||||||
return n1
|
return n1
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -132,10 +132,6 @@ describe('compiler: v-once', () => {
|
||||||
id: 0,
|
id: 0,
|
||||||
tag: 'Comp',
|
tag: 'Comp',
|
||||||
once: true,
|
once: true,
|
||||||
},
|
|
||||||
{
|
|
||||||
type: IRNodeTypes.INSERT_NODE,
|
|
||||||
elements: [0],
|
|
||||||
parent: 1,
|
parent: 1,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
@ -51,6 +51,7 @@ export function genCreateComponent(
|
||||||
const rawSlots = genRawSlots(slots, context)
|
const rawSlots = genRawSlots(slots, context)
|
||||||
const [ids, handlers] = processInlineHandlers(props, context)
|
const [ids, handlers] = processInlineHandlers(props, context)
|
||||||
const rawProps = context.withId(() => genRawProps(props, context), ids)
|
const rawProps = context.withId(() => genRawProps(props, context), ids)
|
||||||
|
|
||||||
const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
|
const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
|
||||||
(acc, { name, value }) => {
|
(acc, { name, value }) => {
|
||||||
const handler = genEventHandler(context, value, undefined, false)
|
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 type { CodegenContext } from '../generate'
|
||||||
import { genInsertNode, genPrependNode } from './dom'
|
import { genInsertNode, genPrependNode } from './dom'
|
||||||
import { genSetDynamicEvents, genSetEvent } from './event'
|
import { genSetDynamicEvents, genSetEvent } from './event'
|
||||||
|
@ -14,6 +20,7 @@ import {
|
||||||
INDENT_START,
|
INDENT_START,
|
||||||
NEWLINE,
|
NEWLINE,
|
||||||
buildCodeFragment,
|
buildCodeFragment,
|
||||||
|
genCall,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { genCreateComponent } from './component'
|
import { genCreateComponent } from './component'
|
||||||
import { genSlotOutlet } from './slotOutlet'
|
import { genSlotOutlet } from './slotOutlet'
|
||||||
|
@ -26,6 +33,9 @@ export function genOperations(
|
||||||
): CodeFragment[] {
|
): CodeFragment[] {
|
||||||
const [frag, push] = buildCodeFragment()
|
const [frag, push] = buildCodeFragment()
|
||||||
for (const operation of opers) {
|
for (const operation of opers) {
|
||||||
|
if (isTypeThatNeedsInsertionState(operation) && operation.parent) {
|
||||||
|
push(...genInsertionstate(operation, context))
|
||||||
|
}
|
||||||
push(...genOperation(operation, context))
|
push(...genOperation(operation, context))
|
||||||
}
|
}
|
||||||
return frag
|
return frag
|
||||||
|
@ -134,3 +144,21 @@ export function genEffect(
|
||||||
|
|
||||||
return frag
|
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
|
positive: BlockIRNode
|
||||||
negative?: BlockIRNode | IfIRNode
|
negative?: BlockIRNode | IfIRNode
|
||||||
once?: boolean
|
once?: boolean
|
||||||
|
parent?: number
|
||||||
|
anchor?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRFor {
|
export interface IRFor {
|
||||||
|
@ -93,6 +95,8 @@ export interface ForIRNode extends BaseIRNode, IRFor {
|
||||||
once: boolean
|
once: boolean
|
||||||
component: boolean
|
component: boolean
|
||||||
onlyChild: boolean
|
onlyChild: boolean
|
||||||
|
parent?: number
|
||||||
|
anchor?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetPropIRNode extends BaseIRNode {
|
export interface SetPropIRNode extends BaseIRNode {
|
||||||
|
@ -158,6 +162,7 @@ export interface SetTemplateRefIRNode extends BaseIRNode {
|
||||||
effect: boolean
|
effect: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO remove, no longer needed
|
||||||
export interface CreateTextNodeIRNode extends BaseIRNode {
|
export interface CreateTextNodeIRNode extends BaseIRNode {
|
||||||
type: IRNodeTypes.CREATE_TEXT_NODE
|
type: IRNodeTypes.CREATE_TEXT_NODE
|
||||||
id: number
|
id: number
|
||||||
|
@ -198,6 +203,8 @@ export interface CreateComponentIRNode extends BaseIRNode {
|
||||||
root: boolean
|
root: boolean
|
||||||
once: boolean
|
once: boolean
|
||||||
dynamic?: SimpleExpressionNode
|
dynamic?: SimpleExpressionNode
|
||||||
|
parent?: number
|
||||||
|
anchor?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeclareOldRefIRNode extends BaseIRNode {
|
export interface DeclareOldRefIRNode extends BaseIRNode {
|
||||||
|
@ -211,6 +218,8 @@ export interface SlotOutletIRNode extends BaseIRNode {
|
||||||
name: SimpleExpressionNode
|
name: SimpleExpressionNode
|
||||||
props: IRProps[]
|
props: IRProps[]
|
||||||
fallback?: BlockIRNode
|
fallback?: BlockIRNode
|
||||||
|
parent?: number
|
||||||
|
anchor?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetTextChildIRNode extends BaseIRNode {
|
export interface GetTextChildIRNode extends BaseIRNode {
|
||||||
|
@ -288,3 +297,21 @@ export type VaporDirectiveNode = Overwrite<
|
||||||
arg: Exclude<DirectiveNode['arg'], CompoundExpressionNode>
|
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,
|
type TransformContext,
|
||||||
transformNode,
|
transformNode,
|
||||||
} from '../transform'
|
} 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) => {
|
export const transformChildren: NodeTransform = (node, context) => {
|
||||||
const isFragment =
|
const isFragment =
|
||||||
|
@ -66,21 +71,11 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
|
||||||
if (prevDynamics.length) {
|
if (prevDynamics.length) {
|
||||||
if (hasStaticTemplate) {
|
if (hasStaticTemplate) {
|
||||||
context.childrenTemplate[index - prevDynamics.length] = `<!>`
|
context.childrenTemplate[index - prevDynamics.length] = `<!>`
|
||||||
|
|
||||||
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
|
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
|
||||||
const anchor = (prevDynamics[0].anchor = context.increaseId())
|
const anchor = (prevDynamics[0].anchor = context.increaseId())
|
||||||
context.registerOperation({
|
registerInsertion(prevDynamics, context, anchor)
|
||||||
type: IRNodeTypes.INSERT_NODE,
|
|
||||||
elements: prevDynamics.map(child => child.id!),
|
|
||||||
parent: context.reference(),
|
|
||||||
anchor,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
context.registerOperation({
|
registerInsertion(prevDynamics, context, -1 /* prepend */)
|
||||||
type: IRNodeTypes.PREPEND_NODE,
|
|
||||||
elements: prevDynamics.map(child => child.id!),
|
|
||||||
parent: context.reference(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
prevDynamics = []
|
prevDynamics = []
|
||||||
}
|
}
|
||||||
|
@ -89,10 +84,32 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevDynamics.length) {
|
if (prevDynamics.length) {
|
||||||
|
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({
|
context.registerOperation({
|
||||||
type: IRNodeTypes.INSERT_NODE,
|
type: IRNodeTypes.INSERT_NODE,
|
||||||
elements: prevDynamics.map(child => child.id!),
|
elements: dynamics.map(child => child.id!),
|
||||||
parent: context.reference(),
|
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 { type SSRContext, renderToString } from '@vue/server-renderer'
|
||||||
import {
|
import {
|
||||||
child,
|
child,
|
||||||
|
createComponent,
|
||||||
createVaporSSRApp,
|
createVaporSSRApp,
|
||||||
delegateEvents,
|
delegateEvents,
|
||||||
next,
|
next,
|
||||||
renderEffect,
|
renderEffect,
|
||||||
setClass,
|
setClass,
|
||||||
|
setInsertionState,
|
||||||
setText,
|
setText,
|
||||||
template,
|
template,
|
||||||
} from '../src'
|
} 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', () => {
|
// test('element with ref', () => {
|
||||||
// const el = ref()
|
// const el = ref()
|
||||||
// const { vnode, container } = mountWithHydration('<div></div>', () =>
|
// const { vnode, container } = mountWithHydration('<div></div>', () =>
|
||||||
|
|
|
@ -58,6 +58,12 @@ import {
|
||||||
getSlot,
|
getSlot,
|
||||||
} from './componentSlots'
|
} from './componentSlots'
|
||||||
import { hmrReload, hmrRerender } from './hmr'
|
import { hmrReload, hmrRerender } from './hmr'
|
||||||
|
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
||||||
|
import {
|
||||||
|
insertionAnchor,
|
||||||
|
insertionParent,
|
||||||
|
resetInsertionState,
|
||||||
|
} from './insertionState'
|
||||||
|
|
||||||
export { currentInstance } from '@vue/runtime-dom'
|
export { currentInstance } from '@vue/runtime-dom'
|
||||||
|
|
||||||
|
@ -136,6 +142,10 @@ export function createComponent(
|
||||||
currentInstance.appContext) ||
|
currentInstance.appContext) ||
|
||||||
emptyContext,
|
emptyContext,
|
||||||
): VaporComponentInstance {
|
): VaporComponentInstance {
|
||||||
|
if (isHydrating) {
|
||||||
|
locateHydrationNode()
|
||||||
|
}
|
||||||
|
|
||||||
// vdom interop enabled and component is not an explicit vapor component
|
// vdom interop enabled and component is not an explicit vapor component
|
||||||
if (appContext.vapor && !component.__vapor) {
|
if (appContext.vapor && !component.__vapor) {
|
||||||
return appContext.vapor.vdomMount(component as any, rawProps, rawSlots)
|
return appContext.vapor.vdomMount(component as any, rawProps, rawSlots)
|
||||||
|
@ -253,6 +263,11 @@ export function createComponent(
|
||||||
|
|
||||||
onScopeDispose(() => unmountComponent(instance), true)
|
onScopeDispose(() => unmountComponent(instance), true)
|
||||||
|
|
||||||
|
if (!isHydrating && insertionParent) {
|
||||||
|
insert(instance.block, insertionParent, insertionAnchor)
|
||||||
|
resetInsertionState()
|
||||||
|
}
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
import { warn } from '@vue/runtime-dom'
|
||||||
|
import {
|
||||||
|
insertionAnchor,
|
||||||
|
insertionParent,
|
||||||
|
resetInsertionState,
|
||||||
|
setInsertionState,
|
||||||
|
} from '../insertionState'
|
||||||
import { child, next } from './node'
|
import { child, next } from './node'
|
||||||
|
|
||||||
export let isHydrating = false
|
export let isHydrating = false
|
||||||
|
@ -10,31 +17,28 @@ export function setCurrentHydrationNode(node: Node | null): void {
|
||||||
let isOptimized = false
|
let isOptimized = false
|
||||||
|
|
||||||
export function withHydration(container: ParentNode, fn: () => void): void {
|
export function withHydration(container: ParentNode, fn: () => void): void {
|
||||||
adoptHydrationNode = adoptHydrationNodeImpl
|
adoptTemplate = adoptTemplateImpl
|
||||||
|
locateHydrationNode = locateHydrationNodeImpl
|
||||||
if (!isOptimized) {
|
if (!isOptimized) {
|
||||||
// optimize anchor cache lookup
|
// optimize anchor cache lookup
|
||||||
const proto = Comment.prototype as any
|
;(Comment.prototype as any).$fs = undefined
|
||||||
proto.$p = proto.$e = undefined
|
|
||||||
isOptimized = true
|
isOptimized = true
|
||||||
}
|
}
|
||||||
isHydrating = true
|
isHydrating = true
|
||||||
currentHydrationNode = child(container)
|
setInsertionState(container, 0)
|
||||||
const res = fn()
|
const res = fn()
|
||||||
|
resetInsertionState()
|
||||||
isHydrating = false
|
isHydrating = false
|
||||||
currentHydrationNode = null
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export let adoptHydrationNode: (
|
export let adoptTemplate: (node: Node, template: string) => Node | null
|
||||||
node: Node | null,
|
export let locateHydrationNode: () => void
|
||||||
template?: string,
|
|
||||||
) => Node | null
|
|
||||||
|
|
||||||
type Anchor = Comment & {
|
type Anchor = Comment & {
|
||||||
// previous open anchor
|
// cached matching fragment start to avoid repeated traversal
|
||||||
$p?: Anchor
|
// on nested fragments
|
||||||
// matching end anchor
|
$fs?: Anchor
|
||||||
$e?: Anchor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isComment = (node: Node, data: string): node is 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
|
* Locate the first non-fragment-comment node and locate the next node
|
||||||
* while handling potential fragments.
|
* while handling potential fragments.
|
||||||
*/
|
*/
|
||||||
function adoptHydrationNodeImpl(
|
function adoptTemplateImpl(node: Node, template: string): Node | null {
|
||||||
node: Node | null,
|
if (!(template[0] === '<' && template[1] === '!')) {
|
||||||
template?: string,
|
|
||||||
): Node | null {
|
|
||||||
if (!isHydrating || !node) {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
let adopted: Node | undefined
|
|
||||||
let end: Node | undefined | null
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
if (template[0] !== '<' && template[1] !== '!') {
|
|
||||||
while (node.nodeType === 8) node = next(node)
|
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) {
|
if (__DEV__) {
|
||||||
const type = adopted.nodeType
|
const type = node.nodeType
|
||||||
if (
|
if (
|
||||||
(type === 8 && !template.startsWith('<!')) ||
|
(type === 8 && !template.startsWith('<!')) ||
|
||||||
(type === 1 &&
|
(type === 1 &&
|
||||||
!template.startsWith(
|
!template.startsWith(`<` + (node as Element).tagName.toLowerCase())) ||
|
||||||
`<` + (adopted as Element).tagName.toLowerCase(),
|
|
||||||
)) ||
|
|
||||||
(type === 3 &&
|
(type === 3 &&
|
||||||
template.trim() &&
|
template.trim() &&
|
||||||
!template.startsWith((adopted as Text).data))
|
!template.startsWith((node as Text).data))
|
||||||
) {
|
) {
|
||||||
// TODO recover and provide more info
|
// TODO recover and provide more info
|
||||||
console.error(`adopted: `, adopted)
|
warn(`adopted: `, node)
|
||||||
console.error(`template: ${template}`)
|
warn(`template: ${template}`)
|
||||||
throw new Error('hydration mismatch!')
|
warn('hydration mismatch!')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentHydrationNode = next(end!)
|
currentHydrationNode = next(node)
|
||||||
return adopted
|
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 {
|
import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
|
||||||
adoptHydrationNode,
|
|
||||||
currentHydrationNode,
|
|
||||||
isHydrating,
|
|
||||||
} from './hydration'
|
|
||||||
import { child, createTextNode } from './node'
|
import { child, createTextNode } from './node'
|
||||||
|
|
||||||
let t: HTMLTemplateElement
|
let t: HTMLTemplateElement
|
||||||
|
@ -16,7 +12,7 @@ export function template(html: string, root?: boolean) {
|
||||||
// TODO this should not happen
|
// TODO this should not happen
|
||||||
throw new Error('No current hydration node')
|
throw new Error('No current hydration node')
|
||||||
}
|
}
|
||||||
return adoptHydrationNode(currentHydrationNode, html)!
|
return adoptTemplate(currentHydrationNode!, html)!
|
||||||
}
|
}
|
||||||
// fast path for text nodes
|
// fast path for text nodes
|
||||||
if (html[0] !== '<') {
|
if (html[0] !== '<') {
|
||||||
|
|
|
@ -6,6 +6,7 @@ export type { VaporDirective } from './directives/custom'
|
||||||
|
|
||||||
// compiler-use only
|
// compiler-use only
|
||||||
export { insert, prepend, remove, isFragment, VaporFragment } from './block'
|
export { insert, prepend, remove, isFragment, VaporFragment } from './block'
|
||||||
|
export { setInsertionState } from './insertionState'
|
||||||
export { createComponent, createComponentWithFallback } from './component'
|
export { createComponent, createComponentWithFallback } from './component'
|
||||||
export { renderEffect } from './renderEffect'
|
export { renderEffect } from './renderEffect'
|
||||||
export { createSlot } from './componentSlots'
|
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