Start Reading the Runtime
Now that we have followed through the implementation that compiles
<template>
<p>Hello, Vapor!</p>
</template>
we can now look at how the resulting code
const _sfc_main = {};
import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
const n0 = t0();
return n0;
}
export default Object.assign(_sfc_main, {
render: _sfc_render,
vapor: true,
__file: "/path/to/App.vue",
});
actually works!
vue/vapor
We've been overlooking it, but we haven't explained this package yet.
This is the entry point for Vapor Mode.
The source code is located in packages/vue/vapor.
There is another entry package for Vapor Mode called packages/vue-vapor, but vue/vapor
simply imports this package.
2 "name": "@vue/vapor",
1export * from '@vue/vapor'
We import the helper functions necessary for the Vapor runtime from this vue/vapor
.
template Function
This is one of the helper functions for Vapor.
import { template } from "vue/vapor";
const t0 = template("<p>Hello, Vapor!</p>");
const n0 = t0();
This is how you declare a template and obtain a Block
.
Let's look at the implementation of the template
function.
2export function template(html: string) {
3 let node: ChildNode
4 const create = () => {
5 // eslint-disable-next-line no-restricted-globals
6 const t = document.createElement('template')
7 t.innerHTML = html
8 return t.content.firstChild!
9 }
10 return (): Node => (node || (node = create())).cloneNode(true)
11}
It stores the string passed as an argument into the innerHTML
of a temporary template
element and obtains the Block
by reading the firstChild
of the template
.
The node created once is kept as a local variable of this function, and subsequent calls result in a cloneNode
.
import { template } from "vue/vapor";
const t0 = template("<p>Hello, Vapor!</p>");
const n0 = t0();
const n1 = t0(); // clone node
As seen in the following code,
function _sfc_render(_ctx) {
const n0 = t0();
return n0;
}
export default Object.assign(_sfc_main, {
render: _sfc_render,
vapor: true,
__file: "/path/to/App.vue",
});
in Vapor Mode, the render function of the component just returns a DOM element for this code.
Application Entry Point
Now, let's understand the application's entry point to see how this component works.
When building an app with Vue.js, it is often written like this:
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
The same applies to Vapor Mode. We use createVaporApp
instead of createApp
.
import { createVaporApp } from "vue/vapor";
import App from "./App.vue";
createVaporApp(App).mount("#app");
In other words, if we read the implementation of createVaporApp
, we can understand how this component works.
createVaporApp
The implementation is located in packages/runtime-vapor/src/apiCreateVaporApp.ts.
22export function createVaporApp(
23 rootComponent: Component,
24 rootProps: RawProps | null = null,
25): App {
It is almost the same as createApp
in runtime-core
.
First, it creates the application's context and creates an App
instance.
This App
instance has a method called mount
.
38 const context = createAppContext()
43 const app: App = (context.app = {
44 _uid: uid++,
45 _component: rootComponent,
46 _props: rootProps,
47 _container: null,
48 _context: context,
49 _instance: null,
50
51 version,
112 mount(rootContainer): any {
There are also component
functions for registering components and use
functions for using plugins.
It is mostly the same as traditional Vue.js.
App.mount
Let's look at the process of the mount
function.
112 mount(rootContainer): any {
113 if (!instance) {
114 rootContainer = normalizeContainer(rootContainer)
115 // #5571
116 if (__DEV__ && (rootContainer as any).__vue_app__) {
117 warn(
118 `There is already an app instance mounted on the host container.\n` +
119 ` If you want to mount another app on the same host container,` +
120 ` you need to unmount the previous app by calling \`app.unmount()\` first.`,
121 )
122 }
123 instance = createComponentInstance(
124 rootComponent,
125 rootProps,
126 null,
127 false,
128 context,
129 )
130 setupComponent(instance)
131 render(instance, rootContainer)
132
133 app._container = rootContainer
134 // for devtools and telemetry
135 ;(rootContainer as any).__vue_app__ = app
136
137 if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
138 app._instance = instance
139 devtoolsInitApp(app, version)
140 }
141
142 return instance
143 } else if (__DEV__) {
144 warn(
145 `App has already been mounted.\n` +
146 `If you want to remount the same app, move your app creation logic ` +
147 `into a factory function and create fresh app instances for each ` +
148 `mount - e.g. \`const createMyApp = () => createApp(App)\``,
149 )
150 }
151 },
It treats the selector or element passed as an argument as a container.
The implementation of the normalizeContainer
function is like this:
114export function normalizeContainer(container: string | ParentNode): ParentNode {
115 return typeof container === 'string'
116 ? (querySelector(container) as ParentNode)
117 : container
118}
After that, it performs createComponentInstance
, setupComponent
, and render
(initial) to complete the mount process.
123 instance = createComponentInstance(
124 rootComponent,
125 rootProps,
126 null,
127 false,
128 context,
129 )
130 setupComponent(instance)
131 render(instance, rootContainer)
createComponentInstance
createComponentInstance
creates an object called ComponentInternalInstance
.
262export function createComponentInstance(
263 component: Component,
264 rawProps: RawProps | null,
265 slots: RawSlots | null,
266 once: boolean = false,
267 // application root node only
268 appContext?: AppContext,
269): ComponentInternalInstance {
151export interface ComponentInternalInstance {
ComponentInternalInstance
holds internal component information, such as registered lifecycle, props, emit information, state, etc. It also holds the definition of the provided component.
191 /**
192 * @internal
193 */
194 [VaporLifecycleHooks.BEFORE_MOUNT]: LifecycleHook
195 /**
196 * @internal
197 */
198 [VaporLifecycleHooks.MOUNTED]: LifecycleHook
199 /**
200 * @internal
201 */
202 [VaporLifecycleHooks.BEFORE_UPDATE]: LifecycleHook
203 /**
204 * @internal
205 */
206 [VaporLifecycleHooks.UPDATED]: LifecycleHook
207 /**
208 * @internal
209 */
210 [VaporLifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
211 /**
212 * @internal
213 */
214 [VaporLifecycleHooks.UNMOUNTED]: LifecycleHook
215 /**
216 * @internal
217 */
218 [VaporLifecycleHooks.RENDER_TRACKED]: LifecycleHook
219 /**
220 * @internal
221 */
222 [VaporLifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
223 /**
224 * @internal
225 */
226 [VaporLifecycleHooks.ACTIVATED]: LifecycleHook
227 /**
228 * @internal
229 */
230 [VaporLifecycleHooks.DEACTIVATED]: LifecycleHook
231 /**
232 * @internal
233 */
234 [VaporLifecycleHooks.ERROR_CAPTURED]: LifecycleHook
167 rawProps: NormalizedRawProps
168 propsOptions: NormalizedPropsOptions
169 emitsOptions: ObjectEmitsOptions | null
170
171 // state
172 setupState: Data
173 setupContext: SetupContext | null
174 props: Data
175 emit: EmitFn
176 emitted: Record<string, boolean> | null
177 attrs: Data
178 slots: StaticSlots
179 refs: Data
180 // exposed properties via expose()
181 exposed?: Record<string, any>
288 type: component,
This is also mostly the same as runtime-core
.
In createComponentInstance
, it not only generates a ComponentInternalInstance
object but also creates an EffectScope
and initializes props
, emit
, and slot
.
360 instance.root = parent ? parent.root : instance
361 instance.scope = new BlockEffectScope(instance, parent && parent.scope)
362 initProps(instance, rawProps, !isFunction(component), once)
363 initSlots(instance, slots)
364 instance.emit = emit.bind(null, instance)
A unique implementation for Vapor is that it holds a block
.
Traditionally, it held a VNode
(virtual DOM) as subTree
or next
, but in Vapor, it holds a Block
.
158 block: Block | null
Traditional:
324 /**
325 * The pending new vnode from parent updates
326 * @internal
327 */
328 next: VNode | null
329 /**
330 * Root vnode of this component's own vdom tree
331 */
332 subTree: VNode
From now on, the Block
will be stored here when rendered.
We'll revisit props, emit, and slot handling when we run components that use them.
We'll skip them for now.
setupComponent
Now, let's move on to the rendering process.
This is the essence of Vapor Mode.
Previously, in the file renderer.ts
, the patch
process of the VNode
was performed.
In Vapor Mode, there is no VNode or patch, so the initial setup process is everything.
Subsequent updates directly manipulate the DOM (Block) via the reactivity system.
For now, since we don't have any state, let's see how the Block obtained from the render function is handled.
This function is located in a file called packages/runtime-vapor/src/apiRender.ts, which implements the rendering processes.
First, as soon as we enter setupComponent
, we set the currentInstance
to the target component.
40 const reset = setCurrentInstance(instance)
Next, various setups are executed within the effectScope generated in createComponentInstance
.
41 instance.scope.run(() => {
We won't go into detail about effectScope since it's part of the Vue.js API, but for those who don't know, it's essentially "a way to collect effects and make them easier to clean up later."
https://vuejs.org/api/reactivity-advanced.html#effectscope
By forming various effects within this scope, we can clean up when the component is unmounted by stopping the effectScope.
155export function unmountComponent(instance: ComponentInternalInstance): void {
156 const { container, block, scope } = instance
157
158 // hook: beforeUnmount
159 invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_UNMOUNT, 'beforeUnmount')
160
161 scope.stop()
Now, let's see what exactly is done within the effectScope.
setupComponent > effectScope
First is handling the setup function.
If the component itself is a function, it is executed as a function component.
If it is an object, the setup function is extracted.
50 const setupFn = isFunction(component) ? component : component.setup
Then, this function is executed.
56 stateOrNode = callWithErrorHandling(
57 setupFn,
58 instance,
59 VaporErrorCodes.SETUP_FUNCTION,
60 [__DEV__ ? shallowReadonly(props) : props, setupContext],
61 )
The result will be either a state or a Node.
If the result is a Node (or a fragment or component), it is stored in a variable called block
.
65 let block: Block | undefined
66
67 if (
68 stateOrNode &&
69 (stateOrNode instanceof Node ||
70 isArray(stateOrNode) ||
71 fragmentKey in stateOrNode ||
72 componentKey in stateOrNode)
73 ) {
74 block = stateOrNode
After this, if there is still nothing in the block
variable, it attempts to obtain the block from the render function.
In this component, it enters this branch, and the render function is executed, storing the block (n0
).
78 if (!block && component.render) {
79 pauseTracking()
80 block = callWithErrorHandling(
81 component.render,
82 instance,
83 VaporErrorCodes.RENDER_FUNCTION,
84 [instance.setupState],
85 )
86 resetTracking()
87 }
At this point, the block is stored in instance.block
.
96 instance.block = block
And that's it for setting up the screen update.
As we'll see when looking at the compilation results of more complex components, most update processes are directly described as effects in the component.
Therefore, rendering a component is as simple as executing the setup function (which defines the state) and generating the block with the render function (where the effect is formed).
All that's left is to mount the block obtained from the render function to the DOM.
render
At the end of https://github.com/vuejs/core-vapor/blob/30583b9ee1c696d3cb836f0bfd969793e57e849d/packages/runtime-vapor/src/apiCreateVaporApp.ts#L123-L131, we have the render
part.
This render
function is an internal function and is different from the render
function of the component.
12 render,
13 setupComponent,
14 unmountComponent,
15} from './apiRender'
Like setupComponent
, it is implemented in packages/runtime-vapor/src/apiRender.ts.
What it does is very simple: it mounts the component and executes the tasks in the queue (scheduler).
(※ You don't need to worry about the scheduler for now.)
106export function render(
107 instance: ComponentInternalInstance,
108 container: string | ParentNode,
109): void {
110 mountComponent(instance, (container = normalizeContainer(container)))
111 flushPostFlushCbs()
112}
mountComponent
is also very simple.
120function mountComponent(
121 instance: ComponentInternalInstance,
122 container: ParentNode,
123) {
It sets the container (the DOM selected from #app
in this case) passed as an argument to instance.container
.
124 instance.container = container
Then, it executes the beforeMount hook.
130 // hook: beforeMount
131 invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_MOUNT, 'beforeMount')
Finally, it inserts the block into the container.
133 insert(instance.block!, instance.container)
(The insert function is really just an insert.)
23export function insert(
24 block: Block,
25 parent: ParentNode,
26 anchor: Node | null = null,
27): void {
28 normalizeBlock(block).forEach(node => parent.insertBefore(node, anchor))
29}
After executing the mounted hook, the component's mount is complete.
135 // hook: mounted
136 invokeLifecycle(
137 instance,
138 VaporLifecycleHooks.MOUNTED,
139 'mounted',
140 instance => (instance.isMounted = true),
141 true,
142 )
Summary
We've seen how the compiled code like
const _sfc_main = {};
import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
const n0 = t0();
return n0;
}
export default Object.assign(_sfc_main, {
render: _sfc_render,
vapor: true,
__file: "/path/to/App.vue",
});
works, which is just to create a component instance, execute the setup function (if available), call the render
function to get the block, and then app.mount(selector)
to insert the block into the selector.
It's very simple.
Now we understand how a SFC like
<template>
<p>Hello, Vapor!</p>
</template>
is compiled and works in the runtime!
The steps are:
- Write a Vue.js SFC.
- Run it through the Vapor Mode compiler.
- Look at the output (get an overview).
- Check the implementation of the compiler.
- Read the output code.
- Go back to step 1.
We have completed up to step 5.
Let's go back to step 1 and look at more complex components in the same way!