Skip to content

Commit 64270ae

Browse files
committed
wip(vapor): basic hydration
1 parent bce7164 commit 64270ae

File tree

6 files changed

+207
-29
lines changed

6 files changed

+207
-29
lines changed

packages/runtime-vapor/src/apiCreateApp.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
unmountComponent,
88
} from './component'
99
import {
10+
type App,
1011
type AppMountFn,
1112
type AppUnmountFn,
1213
type CreateAppFunction,
@@ -20,6 +21,7 @@ import {
2021
import type { RawProps } from './componentProps'
2122
import { getGlobalThis } from '@vue/shared'
2223
import { optimizePropertyLookup } from './dom/prop'
24+
import { withHydration } from './dom/hydrate'
2325

2426
let _createApp: CreateAppFunction<ParentNode, VaporComponent>
2527

@@ -28,6 +30,9 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
2830

2931
// clear content before mounting
3032
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
33+
if (__DEV__ && container.childNodes.length) {
34+
warn('mount target container is not empty and will be cleared.')
35+
}
3136
container.textContent = ''
3237
}
3338

@@ -38,21 +43,38 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
3843
false,
3944
app._context,
4045
)
41-
4246
mountComponent(instance, container)
4347
flushOnAppMount()
4448

45-
return instance
49+
return instance!
50+
}
51+
52+
let _hydrateApp: CreateAppFunction<ParentNode, VaporComponent>
53+
54+
const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
55+
optimizePropertyLookup()
56+
57+
let instance: VaporComponentInstance
58+
withHydration(container, () => {
59+
instance = createComponent(
60+
app._component,
61+
app._props as RawProps,
62+
null,
63+
false,
64+
app._context,
65+
)
66+
mountComponent(instance, container)
67+
flushOnAppMount()
68+
})
69+
70+
return instance!
4671
}
4772

4873
const unmountApp: AppUnmountFn = app => {
4974
unmountComponent(app._instance as VaporComponentInstance, app._container)
5075
}
5176

52-
export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
53-
comp,
54-
props,
55-
) => {
77+
function prepareApp() {
5678
// compile-time feature flags check
5779
if (__ESM_BUNDLER__ && !__TEST__) {
5880
initFeatureFlags()
@@ -63,10 +85,9 @@ export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
6385
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
6486
setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__, target)
6587
}
88+
}
6689

67-
if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, getExposed)
68-
const app = _createApp(comp, props)
69-
90+
function postPrepareApp(app: App) {
7091
if (__DEV__) {
7192
app.config.globalProperties = new Proxy(
7293
{},
@@ -84,5 +105,27 @@ export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
84105
container = normalizeContainer(container) as ParentNode
85106
return mount(container, ...args)
86107
}
108+
}
109+
110+
export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
111+
comp,
112+
props,
113+
) => {
114+
prepareApp()
115+
if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, getExposed)
116+
const app = _createApp(comp, props)
117+
postPrepareApp(app)
118+
return app
119+
}
120+
121+
export const createVaporSSRApp: CreateAppFunction<
122+
ParentNode,
123+
VaporComponent
124+
> = (comp, props) => {
125+
prepareApp()
126+
if (!_hydrateApp)
127+
_hydrateApp = createAppAPI(hydrateApp, unmountApp, getExposed)
128+
const app = _hydrateApp(comp, props)
129+
postPrepareApp(app)
87130
return app
88131
}

packages/runtime-vapor/src/block.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from './component'
88
import { createComment, createTextNode } from './dom/node'
99
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
10+
import { isHydrating } from './dom/hydrate'
1011

1112
export type Block =
1213
| Node
@@ -109,16 +110,23 @@ export function insert(
109110
): void {
110111
anchor = anchor === 0 ? parent.firstChild : anchor
111112
if (block instanceof Node) {
112-
parent.insertBefore(block, anchor)
113+
if (!isHydrating) {
114+
parent.insertBefore(block, anchor)
115+
}
113116
} else if (isVaporComponent(block)) {
114-
mountComponent(block, parent, anchor)
117+
if (block.isMounted) {
118+
insert(block.block!, parent, anchor)
119+
} else {
120+
mountComponent(block, parent, anchor)
121+
}
115122
} else if (isArray(block)) {
116-
for (let i = 0; i < block.length; i++) {
117-
insert(block[i], parent, anchor)
123+
for (const b of block) {
124+
insert(b, parent, anchor)
118125
}
119126
} else {
120127
// fragment
121128
if (block.insert) {
129+
// TODO handle hydration for vdom interop
122130
block.insert(parent, anchor)
123131
} else {
124132
insert(block.nodes, parent, anchor)
@@ -127,6 +135,8 @@ export function insert(
127135
}
128136
}
129137

138+
export type InsertFn = typeof insert
139+
130140
export function prepend(parent: ParentNode, ...blocks: Block[]): void {
131141
let i = blocks.length
132142
while (i--) insert(blocks[i], parent, 0)

packages/runtime-vapor/src/component.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -479,14 +479,10 @@ export function mountComponent(
479479
if (__DEV__) {
480480
startMeasure(instance, `mount`)
481481
}
482-
if (!instance.isMounted) {
483-
if (instance.bm) invokeArrayFns(instance.bm)
484-
insert(instance.block, parent, anchor)
485-
if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
486-
instance.isMounted = true
487-
} else {
488-
insert(instance.block, parent, anchor)
489-
}
482+
if (instance.bm) invokeArrayFns(instance.bm)
483+
insert(instance.block, parent, anchor)
484+
if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
485+
instance.isMounted = true
490486
if (__DEV__) {
491487
endMeasure(instance, `mount`)
492488
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { child, next } from './node'
2+
3+
export let isHydrating = false
4+
export let currentHydrationNode: Node | null = null
5+
6+
export function setCurrentHydrationNode(node: Node | null): void {
7+
currentHydrationNode = node
8+
}
9+
10+
export function withHydration(container: ParentNode, fn: () => void): void {
11+
adoptHydrationNode = adoptHydrationNodeImpl
12+
isHydrating = true
13+
currentHydrationNode = child(container)
14+
const res = fn()
15+
isHydrating = false
16+
currentHydrationNode = null
17+
return res
18+
}
19+
20+
export let adoptHydrationNode: (
21+
node: Node | null,
22+
template?: string,
23+
) => Node | null
24+
25+
type Anchor = Comment & {
26+
// previous open anchor
27+
$p?: Anchor
28+
// matching end anchor
29+
$e?: Anchor
30+
}
31+
32+
const isComment = (node: Node, data: string): node is Anchor =>
33+
node.nodeType === 8 && (node as Comment).data === data
34+
35+
/**
36+
* Locate the first non-fragment-comment node and locate the next node
37+
* while handling potential fragments.
38+
*/
39+
function adoptHydrationNodeImpl(
40+
node: Node | null,
41+
template?: string,
42+
): Node | null {
43+
if (!isHydrating || !node) {
44+
return node
45+
}
46+
47+
let adopted: Node | undefined
48+
let end: Node | undefined | null
49+
50+
if (template) {
51+
while (node.nodeType === 8) node = next(node)
52+
adopted = end = node
53+
} else if (isComment(node, '[')) {
54+
// fragment
55+
let start = node
56+
let cur: Node = node
57+
let fragmentDepth = 1
58+
// previously recorded fragment end
59+
if (!end && node.$e) {
60+
end = node.$e
61+
}
62+
while (true) {
63+
cur = next(cur)
64+
if (isComment(cur, '[')) {
65+
// previously recorded fragment end
66+
if (!end && node.$e) {
67+
end = node.$e
68+
}
69+
fragmentDepth++
70+
cur.$p = start
71+
start = cur
72+
} else if (isComment(cur, ']')) {
73+
fragmentDepth--
74+
// record fragment end on start node for later traversal
75+
start.$e = cur
76+
start = start.$p!
77+
if (!fragmentDepth) {
78+
// fragment end
79+
end = cur
80+
break
81+
}
82+
} else if (!adopted) {
83+
adopted = cur
84+
if (end) {
85+
break
86+
}
87+
}
88+
}
89+
if (!adopted) {
90+
throw new Error('hydration mismatch')
91+
}
92+
} else {
93+
adopted = end = node
94+
}
95+
96+
if (__DEV__ && template) {
97+
const type = adopted.nodeType
98+
if (
99+
type === 8 ||
100+
(type === 1 &&
101+
!template.startsWith(
102+
`<` + (adopted as Element).tagName.toLowerCase(),
103+
)) ||
104+
(type === 3 && !template.startsWith((adopted as Text).data))
105+
) {
106+
// TODO recover
107+
throw new Error('hydration mismatch!')
108+
}
109+
}
110+
111+
currentHydrationNode = next(end!)
112+
return adopted
113+
}

packages/runtime-vapor/src/dom/template.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
1+
import {
2+
adoptHydrationNode,
3+
currentHydrationNode,
4+
isHydrating,
5+
} from './hydrate'
6+
import { child } from './node'
7+
8+
let t: HTMLTemplateElement
9+
110
/*! #__NO_SIDE_EFFECTS__ */
211
export function template(html: string, root?: boolean) {
3-
let node: ChildNode
4-
const create = () => {
5-
const t = document.createElement('template')
6-
t.innerHTML = html
7-
return t.content.firstChild!
8-
}
12+
let node: Node
913
return (): Node & { $root?: true } => {
10-
const ret = (node || (node = create())).cloneNode(true)
14+
if (isHydrating) {
15+
if (__DEV__ && !currentHydrationNode) {
16+
// TODO this should not happen
17+
throw new Error('No current hydration node')
18+
}
19+
return adoptHydrationNode(currentHydrationNode, html)!
20+
}
21+
if (!node) {
22+
t = t || document.createElement('template')
23+
t.innerHTML = html
24+
node = child(t.content)
25+
}
26+
const ret = node.cloneNode(true)
1127
if (root) (ret as any).$root = true
1228
return ret
1329
}

packages/runtime-vapor/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// public APIs
2-
export { createVaporApp } from './apiCreateApp'
2+
export { createVaporApp, createVaporSSRApp } from './apiCreateApp'
33
export { defineVaporComponent } from './apiDefineComponent'
44
export { vaporInteropPlugin } from './vdomInterop'
55
export type { VaporDirective } from './directives/custom'

0 commit comments

Comments
 (0)