Build your Own Vue

We are going to rewrite Vue from scratch. Step by step. Following the architecture from the real Vue3 code but without all the optimizations and non-essential features.Besides helping you understand how Vue works, one of the goals of this post is to make it easier for you to dive deeper in the Vue codebase.Starting from scratch, these are all the things we’ll add to our version of Vue one by one:

  • Step I: The h Function
  • Step II: CreateApp
  • Step III: Mount
  • Step IV: Reactivity State
  • Step V: Batch Effect
<script setup>
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>

Step Zero: Review

But first let’s review some basic concepts. You can skip this step if you already have a good idea of how Vue, SFC and DOM elements work.

We’ll use this Vue app, just six lines of code. The second line defines a Reactivity State. The fifth line gets a node from the DOM.

Let’s remove all the Vue specific code and replace it with vanilla JavaScript.

On the first line we have a compile-time syntactic sugar in SFC. It isn’t even valid JavaScript, so in order to replace it with vanilla JS, first we need to replace it with valid JS.

Essensially Vue SFC will export a component, Besides some optimizations, that's alll it does.

Now, we have a component with setup function,

But how about the template?

<script>
export default {
setup () {
const count = ref(0)
return {
count
}
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>

Step I: The h Function

This line we have a element, defined with Template.

It is transformed to JS by build tools like @vue/compiler-sfc.

The transformation is usually complex, because it has some features for optimization, but we can ignore those features first, just focus on the core function: replace the code inside the tags with a call to 'h', passing the tag name, the props and the children as parameters, and move these code as a property into the render function of the above object.

'h' creates an object from its arguments. Besides some validations, that’s all it does. So we can safely replace the function call with its output.

And this is what an element is, an object with three properties: type, props and children.

The 'type' is a string that specifies the type of the DOM node we want to create, it’s the tagName you pass to 'document.createElement' when you want to create a HTML element. It can also be an object, but we’ll leave that for Step VII.

'props' is another object, it has all the keys and values from the Template attributes.

'children' usually is an array with more elements. That’s why elements are also trees.

Now, we have a complete component in SFC,

And the next step is render it to screen.

We can use 'createApp' to accomplish it.

<script>
createApp({
setup () {
const count = ref(0)
return {
count
}
},
render() {
return {
type: 'button',
props: {
onClick: () => this.count.value++
},
children: [this.count.value]
}
}
}).mount('#app')
</script>

Step II: CreateApp

'creaetApp' is a API from Vue3, that returns a Vue application. Essentially it's a JavaScript Object with some special properties.

One of the properties is a function named 'mount', it can render dom to container.

So let’s implement it.

In this function, first we deconstruct 'setup' and 'render' from arguments.

Then run the 'setup' function and save the return to 'ctx'.

Finally in 'mount' function, excute render function using 'ctx' as this, it is where Vue chnages DOM.

so let’s do the updates ourselves.

First we create a node using the element type, in this case 'button'.

Then we assign all the element props to that node. Here it’s 'onClick'.

Then we create the nodes for the children. We only have a number as a child so we create a text node.

Using textNode instead of setting innerText will allow us to treat all elements in the same way later.

Finally, we append the text to the dom and the dom to the container.

Finally, there is a ignored function 'ref'.

It from Reactivity API and returns a reactive and mutable ref object, which wraps the parameter with the '.value' attribute.

In this case, we just replace an object with it.

As I just said, 'ref' is a Reactivity API.

Reactivity is one of the most important concepts in Vue.

I'll leave it for Step IV.

And now we have the same app as before, but without using Vue.

<script>
function ref(value) {
return { value }
}
function createApp(options) {
const { setup, render } = options
const ctx = setup()
return {
mount(selector) {
// TODO create dom nodes
},
}
}
createApp({
setup() {
const count = ref(0)
return {
count,
}
},
render() {
return {
type: 'button',
props: {
onClick: () => this.count.value++,
},
children: [this.count.value],
}
},
}).mount('#app')
</script>

Step III: Mount

Let’s go on with the previous code.

This time we’ll focus on how a Vue application mount.

We’ll start by writing our own 'h' function.

Let's do it.

As we saw in the previous step, an element is an object with type, props and children.

The only thing that our function needs to do is create that object.

This is what the 'h' function does.

Next, we will write the 'mount' function.

We get elements by running the 'render' function,

and run a new function for rendering dom.

We start by creating the DOM node using the element type, and then append the new node to the container.

We recursively do the same for each child.

We also need to handle text elements, if the element is a string or number we create a text node instead of a regular node.

Of course we have to ignore its children.

The last thing we need to do here is assign the element props to the node.

Note the events require special handling.

And that’s it. We now have a library that can render Virtual Dom to the DOM.

<script>
function ref(value) {
return { value }
}
}
function createApp(options) {
const { setup, render } = options
const ctx = setup()
return {
mount(selector) {
const element = render.call(ctx)
const container = document.querySelector(selector)
mountElement(element, container)
},
}
}
}

Step IV: Reactivity State

Before we started, we fold irrelevant code.

Reactivity is one of the most important concepts in Vue.

There are many APIs but the principles are the same.

So we just focus on one, the 'ref' function.

In Vue2, the basic API for Reactivity is 'Object.defineProperty', but it has some shortcomings, suck like cannot handle array,unable to process new attributes added to object.

But ES6 provides a new API called 'Proxy'. it perfectly solves the above problems. So Vue3 uses it instead of 'Object.defineProperty'.

So, let's do it.

Through those code, when we access the return value of ref function,

it can be intercepted by the 'get',

in the same way when it is modified it can be intercepted by the 'set'.

So, we can remember something when 'get' function is emitted, and read something when 'set' function is emitted.

In the Vue3's Reactivity system, the something is called 'Effects'. and the remember is called 'track', and the read is called 'trigger'.

The most important dependency to consider is the rendering function, as Vue is a rendering library.

So let's delcare a global variable to store the rendering function in progress,

and remember it in 'track', read it in'trigger'.

In the 'track' function, if 'activeEffect' exists, we'll save it to a special property.

And in the 'trigger' function, we'll try to run it.

So, what we are going to do is keep activeEffect store the rendering funciton.

We leave the work of the mount function to a new function called 'effect',

but considering the update situation, so we need to clear the historical content before each rendering.

Now, we have a reactive application. When we click the button, the number inside will increase by one.

So far so good, everything is gonna be find if you just test demo.

But it is not a good way, because we directly save dependencies to object itself.

So, let's make it more standardized.

Currently, we keep the effects to the object itself, but if a property is no longer used, its associated effects is still keeping. Yeah, That is memory leak.

We can use WeakMap to solve it, because the keys in WeakMap are weakly referenced, it can ensure that if a reactive property is no longer used, its associated dependencies can be garbage collected.

So let's do it, store dependencies between reactive properties and their effects.

in this case, we have a reactivity state called count, essensially it is an object with a property named 'value'.

so, track will store count as a key to WeakMap, and the value is a Map, it will store 'value' and its dependencies 'activeEffect'.

So far so good, but in the real world, the dependencies of reactivity state can change over time,

Suck like the code below

<template>
<div v-if="already">
<login v-if="shouldLogin" />
</div>
</template>

If already is false, no matter how the 'shouldLogin' modified. rendering function is not its dependencies. Otherwise, it is.

So, we should recollect dependencies before running effects function. Let‘s do it.

First of all, we add a new function called 'cleanup', run it every time before current effect function executed.

Compatible with 'track' and 'trigger' functions,

the former adds the 'deps' to 'activeEffect';

and the latter clone effects before execute them,

this because each effect will cleanup before executed, and the cleanup will remove current effect from 'effects' set,

so cloning ensures that every effects will be executed.

And that’s all.

<script>
let activeEffect = void 0
const targetMap = new WeakMap()
}
}
}
}
}
}
createApp({
setup() {
const count = ref(0)
return {
count,
}
},
render() {
return h(
'button',
{
onClick: () => this.count.value++,
},
[this.count.value]
)
},
}).mount('#app')
</script>

Step V: Batch Effect

Now, we have a Reactivity State, let’s add some changes to onClick.

We add a 'console.log' where effect is called.

And in the onClick function, we let 'count' increase by four times.

Save the changes, we can see 'console.log' will execute four times,

rendering
rendering
rendering
rendering

that means no matter how many times we modify the Reactivity State, the rendering function will be triggered every time.

It's not a good experience.

We expect the rendering function will be triggered only one time within a certain period of time, no matter how many times the Reactivity State is modified.

As we can see, there are three new variables, all of which are used in the renderEffect function.

  • scheduling is a lock,it is used to ensure something can be executed only one time within a certain period of time.

  • queue is an Array that stores effects functions.

  • microTask is a Promise object that is resolved. it is used to execute effects.

When we execute 'renderEffect' with the same parameter, of course the parameter is a function, it will only be executed once in an event loop.

So, let's add it to where effects is called.

So, let's try it by the pervious code, and we can find, there is only one log now.

rendering

That’s all. We've built our own version of Vue.