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 nodeAs 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]: LifecycleHook167 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 | nullTraditional:
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: VNodeFrom 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.setupThen, 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 = stateOrNodeAfter 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 = blockAnd 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/vue-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 = containerThen, 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!
