Dissecting Vue 3: The Mounting Process I

Angel Sola
Feb 14 · 7 min read

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.

Miss Dissection

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:

Vue 3 source packages

In a first dissection, Vue appears to have three major blocks:

  1. the reactivity system (reactivity package),
  2. the compiler (compiler-core, compiler-dom, and compiler-sfc packages) and
  3. the runtime (runtime-core and runtime-dom packages).
Vue is roughly made of three big blocks: the reactivity system, the compiler, and the runtime

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:

Vue 3 createApp function (with some elided details)

This function does two important things:

  1. Creates the app by calling baseCreateApp (line 2, we’ll get to this shortly)
  2. 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 👏🏼.

glovo-engineering

Discover some of the challenges and funny stories we encounter every day at Glovo

Thanks to Juan Carlos Arocha and Lorenzo Arribas

Angel Sola

Written by

glovo-engineering

Discover some of the challenges and funny stories we encounter every day at Glovo

More From Medium

More from glovo-engineering

More from glovo-engineering

More from glovo-engineering

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade