Dissecting Vue 3: Template Compilation
In our last post we dissected the mounting this simple application:
<div id="app">
<h1>Hello, {{ name }}</h1>
</div><script>
const { createApp } = Vue createApp({
data: () => ({ name: 'Rick' })
}).mount('#app')
</script>
Inspecting the source code we discovered that mounting is a two steps process:
mount app = create vNode + render
We took a look at the first step, the generation of the application’s top component as a vNode. Today, we’ll analyze the rendering, concretely, the part where a render function is derived from a component’s template.
Template Compilation
To render a component, Vue compiles its template into a render function, which is a function used to produce the component’s final HTML. This compilation is a three-step process. Eliding some details, the compile
function looks like follows:
function compile(template) {
const ast = parse(template)
transform(ast, transformations)
return generate(ast)
}
Vue first parses the template into an AST in a process known as the template parsing:
parse(template) = AST
This AST is a tree representation of the template that Vue uses to perform optimizations like for instance, figuring out what branches are static and therefore won’t change. These static branches can be safely ignored by Vue in the change detection process.
Every node in the AST has a type
property, which instructs Vue about the nature of it. You can look up in the source code all available node types. Depending on the type, nodes have a different set of properties. Here’s a diagram of the parsed AST for our simple application’s component, with only the basic data in each node:
The AST then undergoes some transformations, like those applied by directives. Lastly, the transformed AST is used to generate the code for a render function:
generate(AST) = Render Function’s Code
This three-step process is better understood with a diagram:
Render Function
When executed, a render function produces the complete virtual node tree representation of a component. The resulting vNode tree is later used to patch the DOM every time a change happens in the component.
render function → Component’s vNode Tree
Let’s do a quick debugging exercise with our simple app to see what the render function looks like for the “hello” component.
Transforming the Template into a Render Function
Let’s open our app in Chrome and set a debug breakpoint inside the finishComponentSetup
function, where the component’s template gets compiled:
In this function, if the component has a template assigned and therender
attribute isn’t present, a render function is generated by the compile
function and assigned to the component. This compile
references the compileToFunction
function defined inside the vue package.
After this template compilation step, the component’s render
function is set, as we can see in Chrome’s debugger:
Let’s check what this function looks like. Chrome gives us the link to the compiled function, which we can follow by clicking on the value to the right of [[FunctionLocation]]
:VM349:formatted:4
in the figure above.
Here’s the function’s code to render our simple hello component:
Inside the top-level function, a function named render
is defined and returned. This inner function extends the resolution scope chain by prepending it the this
context by means of the with
block (line 5).
Let’s take a quick look at an example of using the “with” block, as it’s not something we, JS developers, use very often. The with
block includes in the scope whatever is passed to it:
const person = { name: 'Morty' }with (person) {
// Here, we gain access to person
console.log(`Hi, ${name}`)
}
Notice how we didn’t need to specify person.name
, but only name
instead. That’s because the resolution scope within the block has been extended to include the person
object.
Thethis
inside the with block
is bound to the component the function renders. To be more precise, it’s bound not directly to the component, but to a Proxy object targeting the component. By extending the resolution scope with the component’s proxy, we gain access to its props/data/methods, etc… which explains why the render
function has access to the name
defined in the component (line 15).
So our “<h1>Hello, {{ name }}</h1>”
template got transformed into a render function whose execution generates a virtual DOM which is used to figure out what changes need to be patched into the real DOM.
Let’s represent this template compilation step visually:
You can find this compileToFunction
function inside the vue package, in its index.ts file.
If you want to explore how the render function’s code for a given template looks like, you have this awesome template explorer app online:
The code for the template explorer is also part of Vue’s repo. You can find it inside the template-explorer package. Note that, the online version of the app shows the code that Vue2 compiles, which is slightly different from that in Vue 3.
Quick Recap
In this second post about dissecting the Vue 3 source code, we took a look at the template compilation process. This process, comprised of three stages, is in charge of transforming a component’s template into a render function.
A render function for a component generates its virtual DOM, which is later used by Vue to figure out what changes need to be applied to the HTML document.