Dissecting Vue 3: The Mounting Process
Writing Vue applications doesn’t require knowledge of its inner workings. If you want to deepen your knowledge on the library and understand how its reactivity system works, troubleshoot template compilation errors or maybe optimizing your apps, this post is for you!
This is the first of a series where we’ll dissect Vue 3 source code to understand how it works. Vue 3 hasn’t been officially released yet, but we can expect it anytime now. The library has been rewritten using Typescript, which makes the analysis much easier, and comes packed with lots of performance improvements.
Before we start
For us to understand the source code, we’ll often elide some of its parts so we can focus on what we’re discussing and avoid unnecessary details. We may get into some of those details in future posts.
The source code relies on compilation flags, such as __DEV__
or __RUNTIME_COMPILE__
. We’ll remove them from the source presented in this analysis to avoid their noise.
Vue 3 project structure
The project (now hosted at https://github.com/vuejs/vue-next.git) is very well structured and split into Yarn workspaces, an interesting way of modularizing big projects. The source is distributed among packages:
In a first dissection, Vue appears to have three major blocks:
- the reactivity system (reactivity package),
- the compiler (compiler-core, compiler-dom, and compiler-sfc packages) and
- the runtime (runtime-core and runtime-dom packages).
The project’s contributing guide gives a quick overview of what each of the packages is for. I recommend you take a look at it. Let’s focus our attention on the application mounting process, a process that spans across the runtime and compiler domains.
The app’s mounting process
What does mounting mean? A component is considered to be mounted once the virtual nodes representing its view have been created. These virtual nodes are an in-memory representation of the final DOM the application will render.
Note that the children components needn’t be mounted for a component to be considered mounted, as stated by the documentation. Don’t forget about this; a component shouldn’t rely on its children being mounted by the time its mounted
lifecycle hook is called.
Let’s imagine we have a simple Vue application:
<div id="app">
<h1>Hello, {{ name }}</h1>
</div><script>
const { createApp } = Vue createApp({
data: () => ({ name: 'Rick' })
}).mount('#app')
</script>
The first thing we need to understand is that Vue 3 apps are created passing the createApp
function the application’s top-level component. If you come from Vue 2 you’re probably used to the new Vue({ … })
syntax, but this has changed in Vue 3, where the createApp
function is used to construct the application instance.
Then comes the mounting of the app, which is, in essence, the mounting of this parent component plus some small additions; let’s see what these are. This mount
method is defined inside the createApp
function itself, in the runtime-dom / index.ts file. Here’s the code with some details elided:
This function does two important things:
- Creates the app by calling
baseCreateApp
(line 2, we’ll get to this shortly) - Extends its
mount
method to include two extra steps: grab the component’s container (line 7) and use its inner HTML for the template (line 12).
Lastly, the original mount
method is called (line 17).
The mount method
So far we haven’t unveiled anything too surprising. As you may have already realized, the magic happens inside the original mount
method, which is defined in the app object returned by baseCreateApp
function. This function’s origin can be found a few lines above, in the same file:
const {
render: baseRender,
createApp: baseCreateApp
} = createRenderer({ ... })
This createRenderer
function returns both a renderer
and the createApp
function. This createApp
function internally uses the returned renderer to render the application components.
A big part of Vue’s runtime magic is defined in this createRenderer
function’s 1600 lines of code. This is such a complicated piece of logic that we’ll leave its full dissection for a future post.
What’s inside the original mount
method? You can find these inside the createAppAPI
function located in runtime-core / apiCreateApp.ts. Simplifying its content to the minimum expression, the mount
method looks like the following:
mount(rootContainer: HostElement) {
const vnode = createVNode(rootComponent, rootProps)
render(vnode, rootContainer)
return vnode.component.proxy
}
being rootComponent
and rootProps
the arguments passed to the createApp
function, in which body this mount
function is defined:
return function createApp(rootComponent, rootProps) {
...
mount(rootContainer) { ... }
}
Okay, so, mounting the application basically means creating a virtual node for the rootComponent
and rendering it inside the rootContainer
. The returned value is the vnode
component proxy.
mount app = create vNode + render
A call tree diagram (including the source files where the functions are defined) for the simplified mount
method we’ve just seen is the following:
Let’s try to understand what the first of these two steps means. We’ll dissect the render
function in the second part of this post.
The Virtual Node
So the first in the mount
function was to create a virtual node representing the rootComponent
, that is, the application’s top-level component. What is this virtual node thing? In short, it’s a representation of a node in the document’s DOM. It’s not an actual DOM node, such as a <div>
, but a way of representing it with a JS object.
You’ve probably already heard about virtual DOMs, a performance optimization for web app rendering that batches DOM updates to keep them to a minimum: HTML DOM changes are expensive. If we have an in-memory representation of the DOM made of plain JS objects, we can figure out the changes required in the actual (not virtual) DOM, batch them together, and make only the minimum necessary updates to the document at given moments in time determined by the scheduler (we’ll dissect the scheduler as well in a future post). This process is known as “patching the DOM”.
So, as a first step to understanding how are virtual nodes represented in Vue, let’s take a look at their type definition shown below. Some details have been removed from it.
We’re going to focus our attention on only two properties: type
and component
. If we debug our application and check what the vnode
object looks like once it’s created by createVNode
inside the mount
function:
Chrome’s debugger displays this:
The type
property contains the definition of our root component, including its template
and data
. Had we included props
and methods
, we’d see them here as well. But apart from having the definition of our component, there’s almost nothing else there; the component
property of the vnode
is still null
. Let’s go one step forward and execute the render
function.
If we look at the vnode
after render
has executed we see:
The component
has been set! This property is the internal Vue representation of the top-level component (you can check its type definition here), including the lifecycle hooks, data, props, slots… We’ll dedicate an entire future post to dissect this ComponentInternalInstance
interface and its inner workings.
We’ll leave it here for today. In the second part of this post, we’ll cover the render
function, the one which appears to do all the heavy lifting.
Quick Recap
In this first post about dissecting the Vue 3 source code we took a look at the project’s code structure and found out about it’s three main pillars: the runtime, the compiler, and the reactivity system.
Then, we started looking at the main steps in the application mounting process. We used call trees (instead of stacks) to get a grasp on what functions are involved in the mounting of a very simple application consisting of one component.
We finally learned that Vue uses a virtual DOM to figure out the changes required to be rendered, and now we’re ready for the next step: understanding the render process.
In part II of this post…
We’ll be dissecting the render
function in the mounting process. Don’t miss out as there are a lot of very interesting details we’ll be unveiling.
If you liked the post and want to see more like this in the future, don’t forget to 👏🏼.