wip: refactor

This commit is contained in:
daiwei 2025-04-25 15:08:19 +08:00
parent 34b9a4b5cd
commit aad75fd7c4
25 changed files with 760 additions and 705 deletions

View File

@ -39,6 +39,7 @@ describe('ssr: components', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent("foo"), _mergeProps({ prop: "b" }, _attrs), null), _parent) _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent("foo"), _mergeProps({ prop: "b" }, _attrs), null), _parent)
_push(\`<!--dynamic-component-->\`)
}" }"
`) `)
@ -49,6 +50,7 @@ describe('ssr: components', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: "b" }, _attrs), null), _parent) _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: "b" }, _attrs), null), _parent)
_push(\`<!--dynamic-component-->\`)
}" }"
`) `)
}) })
@ -245,7 +247,7 @@ describe('ssr: components', () => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!--]--></div>\`) _push(\`<!--]--></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -269,7 +271,7 @@ describe('ssr: components', () => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!--]--></div>\`) _push(\`<!--]--></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -363,7 +365,7 @@ describe('ssr: components', () => {
_push(\`\`) _push(\`\`)
if (false) { if (false) {
_push(\`<div\${_scopeId}></div>\`) _push(\`<div\${_scopeId}></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -29,7 +29,7 @@ describe('ssr: attrs fallthrough', () => {
_push(\`<!--[-->\`) _push(\`<!--[-->\`)
if (true) { if (true) {
_push(\`<div></div>\`) _push(\`<div></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -70,7 +70,7 @@ describe('ssr: inject <style vars>', () => {
const _cssVars = { style: { color: _ctx.color }} const _cssVars = { style: { color: _ctx.color }}
if (_ctx.ok) { if (_ctx.ok) {
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`) _push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!--[--><div\${ _push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars) _ssrRenderAttrs(_cssVars)

View File

@ -153,7 +153,7 @@ describe('ssr: <slot>', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (true) { if (true) {
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true) _ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -54,7 +54,7 @@ describe('transition-group', () => {
}) })
if (false) { if (false) {
_push(\`<div></div>\`) _push(\`<div></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} }
_push(\`</ul>\`) _push(\`</ul>\`)
}" }"
@ -124,7 +124,7 @@ describe('transition-group', () => {
}) })
if (_ctx.ok) { if (_ctx.ok) {
_push(\`<div>ok</div>\`) _push(\`<div>ok</div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} }
_push(\`<!--]-->\`) _push(\`<!--]-->\`)
}" }"

View File

@ -8,7 +8,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -24,7 +24,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -40,7 +40,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
} }
@ -56,10 +56,10 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else if (_ctx.bar) { } else if (_ctx.bar) {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -75,10 +75,10 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else if (_ctx.bar) { } else if (_ctx.bar) {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`) _push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
} }
@ -93,7 +93,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[-->hello<!--]-->\`) _push(\`<!--[-->hello<!--]-->\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -110,7 +110,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -127,7 +127,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`) _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -148,7 +148,7 @@ describe('ssr: v-if', () => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]-->\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -167,7 +167,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`) _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
} }

View File

@ -91,7 +91,7 @@ describe('ssr: v-model', () => {
? _ssrLooseContain(_ctx.model, _ctx.i) ? _ssrLooseContain(_ctx.model, _ctx.i)
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : "" : _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
}></option>\`) }></option>\`)
_push(\`<!--$-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -21,7 +21,12 @@ import {
isText, isText,
processExpression, processExpression,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { escapeHtml, isString } from '@vue/shared' import {
DYNAMIC_END_ANCHOR_LABEL,
DYNAMIC_START_ANCHOR_LABEL,
escapeHtml,
isString,
} from '@vue/shared'
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers' import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
import { ssrProcessIf } from './transforms/ssrVIf' import { ssrProcessIf } from './transforms/ssrVIf'
import { ssrProcessFor } from './transforms/ssrVFor' import { ssrProcessFor } from './transforms/ssrVFor'
@ -161,7 +166,7 @@ export function processChildren(
asDynamic = false, asDynamic = false,
): void { ): void {
if (asDynamic) { if (asDynamic) {
context.pushStringPart(`<!--[[-->`) context.pushStringPart(`<!--${DYNAMIC_START_ANCHOR_LABEL}-->`)
} }
if (asFragment) { if (asFragment) {
context.pushStringPart(`<!--[-->`) context.pushStringPart(`<!--[-->`)
@ -259,7 +264,7 @@ export function processChildren(
context.pushStringPart(`<!--]-->`) context.pushStringPart(`<!--]-->`)
} }
if (asDynamic) { if (asDynamic) {
context.pushStringPart(`<!--]]-->`) context.pushStringPart(`<!--${DYNAMIC_END_ANCHOR_LABEL}-->`)
} }
} }

View File

@ -55,7 +55,14 @@ import {
ssrProcessTransitionGroup, ssrProcessTransitionGroup,
ssrTransformTransitionGroup, ssrTransformTransitionGroup,
} from './ssrTransformTransitionGroup' } from './ssrTransformTransitionGroup'
import { extend, isArray, isObject, isPlainObject, isSymbol } from '@vue/shared' import {
DYNAMIC_COMPONENT_ANCHOR_LABEL,
extend,
isArray,
isObject,
isPlainObject,
isSymbol,
} from '@vue/shared'
import { buildSSRProps } from './ssrTransformElement' import { buildSSRProps } from './ssrTransformElement'
import { import {
ssrProcessTransition, ssrProcessTransition,
@ -264,6 +271,8 @@ export function ssrProcessComponent(
// dynamic component (`resolveDynamicComponent` call) // dynamic component (`resolveDynamicComponent` call)
// the codegen node is a `renderVNode` call // the codegen node is a `renderVNode` call
context.pushStatement(node.ssrCodegenNode) context.pushStatement(node.ssrCodegenNode)
// anchor for dynamic component for vapor hydration
context.pushStringPart(`<!--${DYNAMIC_COMPONENT_ANCHOR_LABEL}-->`)
} }
} }
} }

View File

@ -14,6 +14,7 @@ import {
type SSRTransformContext, type SSRTransformContext,
processChildrenAsStatement, processChildrenAsStatement,
} from '../ssrCodegenTransform' } from '../ssrCodegenTransform'
import { IF_ANCHOR_LABEL } from '@vue/shared'
// Plugin for the first transform pass, which simply constructs the AST node // Plugin for the first transform pass, which simply constructs the AST node
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform( export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
@ -80,7 +81,10 @@ function processIfBranch(
needFragmentWrapper, needFragmentWrapper,
) )
if (branch.condition) { if (branch.condition) {
statement.body.push(createCallExpression(`_push`, ['`<!--$-->`'])) // v-if/v-else-if anchor for vapor hydration
statement.body.push(
createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
)
} }
return statement return statement
} }

View File

@ -598,14 +598,14 @@ describe('SSR hydration', () => {
const ctx: SSRContext = {} const ctx: SSRContext = {}
container.innerHTML = await renderToString(h(App), ctx) container.innerHTML = await renderToString(h(App), ctx)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--$--></div>', '<div><!--teleport start--><!--teleport end--><!--if--></div>',
) )
teleportContainer.innerHTML = ctx.teleports!['#target'] teleportContainer.innerHTML = ctx.teleports!['#target']
// hydrate // hydrate
createSSRApp(App).mount(container) createSSRApp(App).mount(container)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--$--></div>', '<div><!--teleport start--><!--teleport end--><!--if--></div>',
) )
expect(teleportContainer.innerHTML).toBe( expect(teleportContainer.innerHTML).toBe(
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->', '<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
@ -614,7 +614,7 @@ describe('SSR hydration', () => {
toggle.value = false toggle.value = false
await nextTick() await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>') expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
}) })
@ -657,21 +657,21 @@ describe('SSR hydration', () => {
// server render // server render
container.innerHTML = await renderToString(h(App)) container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--$--></div>', '<div><!--teleport start--><!--teleport end--><!--if--></div>',
) )
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
// hydrate // hydrate
createSSRApp(App).mount(container) createSSRApp(App).mount(container)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--$--></div>', '<div><!--teleport start--><!--teleport end--><!--if--></div>',
) )
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>') expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
expect(`Hydration children mismatch`).toHaveBeenWarned() expect(`Hydration children mismatch`).toHaveBeenWarned()
toggle.value = false toggle.value = false
await nextTick() await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>') expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
}) })

View File

@ -25,6 +25,8 @@ import {
getEscapedCssVarName, getEscapedCssVarName,
includeBooleanAttr, includeBooleanAttr,
isBooleanAttr, isBooleanAttr,
isDynamicAnchor,
isDynamicFragmentEndAnchor,
isKnownHtmlAttr, isKnownHtmlAttr,
isKnownSvgAttr, isKnownSvgAttr,
isOn, isOn,
@ -84,14 +86,6 @@ const getContainerType = (
return undefined return undefined
} }
export function isDynamicAnchor(node: Node): node is Comment {
return isComment(node) && (node.data === '[[' || node.data === ']]')
}
export function isDynamicFragmentEndAnchor(node: Node): node is Comment {
return isComment(node) && node.data === '$'
}
export const isComment = (node: Node): node is Comment => export const isComment = (node: Node): node is Comment =>
node.nodeType === DOMNodeTypes.COMMENT node.nodeType === DOMNodeTypes.COMMENT
@ -130,8 +124,8 @@ export function createHydrationFunctions(
function nextSibling(node: Node) { function nextSibling(node: Node) {
let n = next(node) let n = next(node)
// skip if: // skip if:
// - dynamic anchors (`<!--[-->`, `<!--]-->`) // - dynamic anchors (`<!--[[-->`, `<!--][-->`)
// - dynamic fragment end anchors (`<!--$-->`) // - dynamic fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
if (n && (isDynamicAnchor(n) || isDynamicFragmentEndAnchor(n))) { if (n && (isDynamicAnchor(n) || isDynamicFragmentEndAnchor(n))) {
n = next(n) n = next(n)
} }

View File

@ -557,7 +557,3 @@ export { startMeasure, endMeasure } from './profiling'
* @internal * @internal
*/ */
export { initFeatureFlags } from './featureFlags' export { initFeatureFlags } from './featureFlags'
/**
* @internal
*/
export { isDynamicAnchor, isDynamicFragmentEndAnchor } from './hydration'

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,8 @@ import {
insertionParent, insertionParent,
resetInsertionState, resetInsertionState,
} from './insertionState' } from './insertionState'
import { isHydrating, locateHydrationNode } from './dom/hydration' import { isHydrating } from './dom/hydration'
import { DYNAMIC_COMPONENT_ANCHOR_LABEL } from '@vue/shared'
export function createDynamicComponent( export function createDynamicComponent(
getter: () => any, getter: () => any,
@ -19,15 +20,9 @@ export function createDynamicComponent(
): VaporFragment { ): VaporFragment {
const _insertionParent = insertionParent const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor const _insertionAnchor = insertionAnchor
if (isHydrating) { if (!isHydrating) resetInsertionState()
locateHydrationNode(true)
} else {
resetInsertionState()
}
const frag = __DEV__ const frag = new DynamicFragment(DYNAMIC_COMPONENT_ANCHOR_LABEL)
? new DynamicFragment('dynamic-component')
: new DynamicFragment()
renderEffect(() => { renderEffect(() => {
const value = getter() const value = getter()
frag.update( frag.update(

View File

@ -1,6 +1,11 @@
import { IF_ANCHOR_LABEL } from '@vue/shared'
import { type Block, type BlockFn, DynamicFragment, insert } from './block' import { type Block, type BlockFn, DynamicFragment, insert } from './block'
import { isHydrating, locateHydrationNode } from './dom/hydration' import { isHydrating } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState' import {
insertionAnchor,
insertionParent,
resetInsertionState,
} from './insertionState'
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
export function createIf( export function createIf(
@ -11,15 +16,13 @@ export function createIf(
): Block { ): Block {
const _insertionParent = insertionParent const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor const _insertionAnchor = insertionAnchor
if (isHydrating) { if (!isHydrating) resetInsertionState()
locateHydrationNode(true)
}
let frag: Block let frag: Block
if (once) { if (once) {
frag = condition() ? b1() : b2 ? b2() : [] frag = condition() ? b1() : b2 ? b2() : []
} else { } else {
frag = __DEV__ ? new DynamicFragment('if') : new DynamicFragment() frag = new DynamicFragment(IF_ANCHOR_LABEL)
renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2)) renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
} }

View File

@ -1,4 +1,4 @@
import { isArray } from '@vue/shared' import { isArray, isDynamicFragmentEndAnchor } from '@vue/shared'
import { import {
type VaporComponentInstance, type VaporComponentInstance,
isVaporComponent, isVaporComponent,
@ -7,8 +7,13 @@ import {
} from './component' } from './component'
import { createComment, createTextNode, nextSiblingAnchor } from './dom/node' import { createComment, createTextNode, nextSiblingAnchor } from './dom/node'
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
import { currentHydrationNode, isComment, isHydrating } from './dom/hydration' import {
import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom' currentHydrationNode,
isComment,
isHydrating,
locateHydrationNode,
} from './dom/hydration'
import { warn } from '@vue/runtime-dom'
export type Block = export type Block =
| Node | Node
@ -39,7 +44,8 @@ export class DynamicFragment extends VaporFragment {
constructor(anchorLabel?: string) { constructor(anchorLabel?: string) {
super([]) super([])
if (isHydrating) { if (isHydrating) {
this.hydrate(anchorLabel) locateHydrationNode(true)
this.hydrate(anchorLabel!)
} else { } else {
this.anchor = this.anchor =
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
@ -81,15 +87,15 @@ export class DynamicFragment extends VaporFragment {
resetTracking() resetTracking()
} }
hydrate(label?: string): void { hydrate(label: string): void {
// for v-if="false" the hydrationNode will be a empty comment node // for v-if="false" the hydrationNode will be a empty comment node
// use it as anchor. // use it as anchor.
// otherwise, use the next sibling comment node as anchor // otherwise, use the next sibling comment node as anchor
if (isComment(currentHydrationNode!, '')) { if (isComment(currentHydrationNode!, '')) {
this.anchor = currentHydrationNode this.anchor = currentHydrationNode
} else { } else {
// find next sibling `<!--$-->` as anchor // find next sibling dynamic fragment end anchor
const anchor = nextSiblingAnchor(currentHydrationNode!, '$')! const anchor = nextSiblingAnchor(currentHydrationNode!, label)!
if (anchor && isDynamicFragmentEndAnchor(anchor)) { if (anchor && isDynamicFragmentEndAnchor(anchor)) {
this.anchor = anchor this.anchor = anchor
} else if (__DEV__) { } else if (__DEV__) {
@ -97,7 +103,6 @@ export class DynamicFragment extends VaporFragment {
warn(`DynamicFragment anchor not found...`) warn(`DynamicFragment anchor not found...`)
} }
} }
if (__DEV__ && label && this.anchor) (this.anchor as Comment).data = label
} }
} }

View File

@ -1,4 +1,4 @@
import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom' import { warn } from '@vue/runtime-dom'
import { import {
insertionAnchor, insertionAnchor,
insertionParent, insertionParent,
@ -12,6 +12,7 @@ import {
next, next,
prev, prev,
} from './node' } from './node'
import { isDynamicFragmentEndAnchor } from '@vue/shared'
export let isHydrating = false export let isHydrating = false
export let currentHydrationNode: Node | null = null export let currentHydrationNode: Node | null = null

View File

@ -1,10 +1,15 @@
import { isDynamicAnchor } from '@vue/runtime-dom'
import { import {
isComment, isComment,
isEmptyText, isEmptyText,
locateEndAnchor, locateEndAnchor,
locateStartAnchor, locateStartAnchor,
} from './hydration' } from './hydration'
import {
DYNAMIC_END_ANCHOR_LABEL,
DYNAMIC_START_ANCHOR_LABEL,
isDynamicAnchor,
isDynamicFragmentEndAnchor,
} from '@vue/shared'
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function createTextNode(value = ''): Text { export function createTextNode(value = ''): Text {
@ -102,8 +107,12 @@ export function disableHydrationNodeLookup(): void {
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function prev(node: Node): Node | null { export function prev(node: Node): Node | null {
// process dynamic node (<!--[[-->...<!--]]-->) as a single one // process dynamic node (<!--[[-->...<!--]]-->) as a single one
if (isComment(node, ']]')) { if (isComment(node, DYNAMIC_END_ANCHOR_LABEL)) {
node = locateStartAnchor(node, '[[', ']]')! node = locateStartAnchor(
node,
DYNAMIC_START_ANCHOR_LABEL,
DYNAMIC_END_ANCHOR_LABEL,
)!
} }
// process fragment node (<!--[-->...<!--]-->) as a single one // process fragment node (<!--[-->...<!--]-->) as a single one
@ -122,21 +131,12 @@ function isNonHydrationNode(node: Node) {
return ( return (
// empty text nodes, no need to hydrate // empty text nodes, no need to hydrate
isEmptyText(node) || isEmptyText(node) ||
// dynamic anchors (<!--[[-->, <!--]]-->) // dynamic node anchors (<!--[[-->, <!--]]-->)
isDynamicAnchor(node) || isDynamicAnchor(node) ||
// fragment end anchor (`<!--]-->`) // fragment end anchor (`<!--]-->`)
isComment(node, ']') || isComment(node, ']') ||
// dynamic fragment anchors // dynamic fragment end anchors
(__DEV__ isDynamicFragmentEndAnchor(node)
? // v-if anchor (`<!--if-->`)
isComment(node, 'if') ||
// v-for anchor (`<!--for-->`)
isComment(node, 'for') ||
// v-slot anchor (`<!--slot-->`)
isComment(node, 'slot') ||
// dynamic-component anchor (`<!--dynamic-component-->`)
isComment(node, 'dynamic-component')
: isComment(node, '$'))
) )
} }
@ -157,8 +157,12 @@ export function nextSiblingAnchor(
function handleWrappedNode(node: Node): Node { function handleWrappedNode(node: Node): Node {
// process dynamic node (<!--[[-->...<!--]]-->) as a single one // process dynamic node (<!--[[-->...<!--]]-->) as a single one
if (isComment(node, '[[')) { if (isComment(node, DYNAMIC_START_ANCHOR_LABEL)) {
return locateEndAnchor(node, '[[', ']]')! return locateEndAnchor(
node,
DYNAMIC_START_ANCHOR_LABEL,
DYNAMIC_END_ANCHOR_LABEL,
)!
} }
// process fragment (<!--[-->...<!--]-->) as a single one // process fragment (<!--[-->...<!--]-->) as a single one

View File

@ -25,7 +25,7 @@ describe('ssr: attr fallthrough', () => {
template: `<child :ok="ok" class="bar"/>`, template: `<child :ok="ok" class="bar"/>`,
} }
expect(await renderToString(createApp(Parent, { ok: true }))).toBe( expect(await renderToString(createApp(Parent, { ok: true }))).toBe(
`<div class="foo bar"></div><!--$-->`, `<div class="foo bar"></div><!--if-->`,
) )
expect(await renderToString(createApp(Parent, { ok: false }))).toBe( expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
`<span class="bar"></span>`, `<span class="bar"></span>`,

View File

@ -14,7 +14,9 @@ describe('ssr: dynamic component', () => {
template: `<component :is="'one'"><span>slot</span></component>`, template: `<component :is="'one'"><span>slot</span></component>`,
}), }),
), ),
).toBe(`<div><!--[--><span>slot</span><!--]--></div>`) ).toBe(
`<div><!--[--><span>slot</span><!--]--></div><!--dynamic-component-->`,
)
}) })
test('resolved to component with v-show', async () => { test('resolved to component with v-show', async () => {
@ -30,7 +32,7 @@ describe('ssr: dynamic component', () => {
}), }),
), ),
).toBe( ).toBe(
`<div><!--[--><div style=\"display:none;\"><!--[-->hi<!--]--></div><!--]--></div>`, `<div><!--[--><div style="display:none;"><!--[-->hi<!--]--></div><!--dynamic-component--><!--]--></div><!--dynamic-component-->`,
) )
}) })
@ -41,7 +43,7 @@ describe('ssr: dynamic component', () => {
template: `<component :is="'p'"><span>slot</span></component>`, template: `<component :is="'p'"><span>slot</span></component>`,
}), }),
), ),
).toBe(`<p><span>slot</span></p>`) ).toBe(`<p><span>slot</span></p><!--dynamic-component-->`)
}) })
test('resolve to component vnode', async () => { test('resolve to component vnode', async () => {
@ -60,7 +62,9 @@ describe('ssr: dynamic component', () => {
template: `<component :is="vnode"><span>slot</span></component>`, template: `<component :is="vnode"><span>slot</span></component>`,
}), }),
), ),
).toBe(`<div>test<!--[--><span>slot</span><!--]--></div>`) ).toBe(
`<div>test<!--[--><span>slot</span><!--]--></div><!--dynamic-component-->`,
)
}) })
test('resolve to element vnode', async () => { test('resolve to element vnode', async () => {
@ -75,6 +79,6 @@ describe('ssr: dynamic component', () => {
template: `<component :is="vnode"><span>slot</span></component>`, template: `<component :is="vnode"><span>slot</span></component>`,
}), }),
), ),
).toBe(`<div id="test"><span>slot</span></div>`) ).toBe(`<div id="test"><span>slot</span></div><!--dynamic-component-->`)
}) })
}) })

View File

@ -94,7 +94,7 @@ describe('ssr: slot', () => {
template: `<one><template v-if="true">hello</template></one>`, template: `<one><template v-if="true">hello</template></one>`,
}), }),
), ),
).toBe(`<div><!--[--><!--[-->hello<!--]--><!--$--><!--]--></div>`) ).toBe(`<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--></div>`)
}) })
test('fragment slot (template v-if + multiple elements)', async () => { test('fragment slot (template v-if + multiple elements)', async () => {
@ -106,7 +106,7 @@ describe('ssr: slot', () => {
}), }),
), ),
).toBe( ).toBe(
`<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--$--><!--]--></div>`, `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--if--><!--]--></div>`,
) )
}) })
@ -135,7 +135,7 @@ describe('ssr: slot', () => {
template: `<one><div v-if="true">foo</div></one>`, template: `<one><div v-if="true">foo</div></one>`,
}), }),
), ),
).toBe(`<div>foo</div><!--$-->`) ).toBe(`<div>foo</div><!--if-->`)
}) })
// #9933 // #9933
@ -170,7 +170,9 @@ describe('ssr: slot', () => {
template: `<ButtonComp><Wrap><div v-if="false">hello</div></Wrap></ButtonComp>`, template: `<ButtonComp><Wrap><div v-if="false">hello</div></Wrap></ButtonComp>`,
}), }),
), ),
).toBe(`<button><!--[--><div><!--[--><!--]--></div><!--]--></button>`) ).toBe(
`<button><!--[--><div><!--[--><!--]--></div><!--]--></button><!--dynamic-component-->`,
)
expect( expect(
await renderToString( await renderToString(
@ -187,7 +189,7 @@ describe('ssr: slot', () => {
}), }),
), ),
).toBe( ).toBe(
`<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button>`, `<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button><!--dynamic-component-->`,
) )
expect( expect(
@ -201,6 +203,6 @@ describe('ssr: slot', () => {
template: `<ButtonComp><template v-if="false">hello</template></ButtonComp>`, template: `<ButtonComp><template v-if="false">hello</template></ButtonComp>`,
}), }),
), ),
).toBe(`<button><!--[--><!--]--></button>`) ).toBe(`<button><!--[--><!--]--></button><!--dynamic-component-->`)
}) })
}) })

View File

@ -264,7 +264,6 @@ export function renderVNode(
renderElementVNode(push, vnode, parentComponent, slotScopeId) renderElementVNode(push, vnode, parentComponent, slotScopeId)
} else if (shapeFlag & ShapeFlags.COMPONENT) { } else if (shapeFlag & ShapeFlags.COMPONENT) {
push(renderComponentVNode(vnode, parentComponent, slotScopeId)) push(renderComponentVNode(vnode, parentComponent, slotScopeId))
push(`<!--$-->`) // anchor for vapor hydration
} else if (shapeFlag & ShapeFlags.TELEPORT) { } else if (shapeFlag & ShapeFlags.TELEPORT) {
renderTeleportVNode(push, vnode, parentComponent, slotScopeId) renderTeleportVNode(push, vnode, parentComponent, slotScopeId)
} else if (shapeFlag & ShapeFlags.SUSPENSE) { } else if (shapeFlag & ShapeFlags.SUSPENSE) {

View File

@ -0,0 +1,30 @@
export const DYNAMIC_START_ANCHOR_LABEL = '[['
export const DYNAMIC_END_ANCHOR_LABEL = ']]'
export const IF_ANCHOR_LABEL: string = __DEV__ ? 'if' : '$'
export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = __DEV__
? 'dynamic-component'
: '$2'
export const FOR_ANCHOR_LABEL: string = __DEV__ ? 'for' : '$3'
export const SLOT_ANCHOR_LABEL: string = __DEV__ ? 'slot' : '$4'
export function isDynamicAnchor(node: Node): node is Comment {
if (node.nodeType !== 8) return false
const data = (node as Comment).data
return (
data === DYNAMIC_START_ANCHOR_LABEL || data === DYNAMIC_END_ANCHOR_LABEL
)
}
export function isDynamicFragmentEndAnchor(node: Node): node is Comment {
if (node.nodeType !== 8) return false
const data = (node as Comment).data
return (
data === IF_ANCHOR_LABEL ||
data === FOR_ANCHOR_LABEL ||
data === SLOT_ANCHOR_LABEL ||
data === DYNAMIC_COMPONENT_ANCHOR_LABEL
)
}

View File

@ -13,3 +13,4 @@ export * from './looseEqual'
export * from './toDisplayString' export * from './toDisplayString'
export * from './typeUtils' export * from './typeUtils'
export * from './subSequence' export * from './subSequence'
export * from './domAnchors'