Create Your Own Vue.js From Scratch - Part 4 (Building The Reactivity)

Marc Backes - Apr 25 '20 - - Dev Community

If you like this article, chances are you'd like what I tweet as well. If you are curious, have a look at my Twitter profile. 🚀

This post is the fourth part of a series called Create Your Own Vue.js From Scratch, where I teach you how to create the fundamentals of a reactive framework such as Vue.js. To follow this blog post, I suggest you read about the other parts of this series first.

Roadmap 🚘

  1. Introduction
  2. Virtual DOM basics
  3. Implementing the virtual DOM & rendering
  4. Building reactivity (this post)
  5. Bringing it all together

What is state reactivity?

State reactivity is when we do something (react) when the state of our application (set of variables) changes. We do this in two steps:

  1. Create a "reactive dependency" (We get notified when a variable changes)
  2. Create a "reactive state" (Basically a collection of dependency variables)

1. Building a reactive dependency

Function to watch over changes

For this to work, we first need a function that is executed when a reactive dependency changes. As in Vue, this is called watchEffect; we'll also call our function that.

In our example, this function looks like this:

function watchEffect(fn) {
    activeEffect = fn
    fn()
    activeEffect = null
}
Enter fullscreen mode Exit fullscreen mode

The global variable activeEffect is a temporary variable where we store our function, passed to watchEffect. This is necessary, so we can access the function when itself reads a dependency that refers to that function.

Dependency class

We can see a reactive dependency as a variable that notifies to its subscribers when it's value changes.

  • It can be created with an initial value, so we need a constructor
  • We need to subscribe a function to changes on the dependency. We'll call this depend()
  • We need a to notify subscribed functions of the dependency when the value changes. We'll call this notify()
  • We need to do something when the value gets read or written, so we need a getter and a setter

So our skeleton will look like this:

class Dep {
    // Initialize the value of the reactive dependency
    constructor(value) {}

    // Subscribe a new function as observer to the dependency
    depend() {}

    // Notify subscribers of a value change
    notify() {}

    // Getter of the dependency. Executed when a part of the software reads the value of it.
    get value() {}

    // Setter of the dependency. Executed when the value changes
    set value(newValue) {}
}
Enter fullscreen mode Exit fullscreen mode

The class has two fields: value (value of the dependency) and subscribers (set of subscribed functions).

We implement this step by step.

Constructor

In the constructor, we initialize the two fields.

constructor(value) {
    this._value = value // not `value` because we add getter/setter named value
    this.subscribers = new Set()
}
Enter fullscreen mode Exit fullscreen mode

subscribers needs to be a Set, so we don't repeatedly subscribe to the same function.

Subscribe a function

Here, we need to subscribe a new function as an observer to the dependency. We call this depend.

depend() {
    if (activeEffect) this.subscribers.add(activeEffect)
}
Enter fullscreen mode Exit fullscreen mode

activeEffect is a temporary variable that is set in the watchEffect which is explained later on in this tutorial.

Notify subscribers of a dependency change

When a value changes, we call this function, so we can notify all subscribers when the dependency value changes.

notify() {
    this.subscribers.forEach((subscriber) => subscriber())
}
Enter fullscreen mode Exit fullscreen mode

What we do here is to execute every subscriber. Remember: This is a subscriber is a function.

Getter

In the getter of the dependency, we need to add the activeEffect (function that will be executed when a change in the dependency occurs) to the list of subscribers. In other words, use the depend() method we defined earlier.

As a result, we return the current value.

get value() {
    this.depend()
    return this._value
}
Enter fullscreen mode Exit fullscreen mode

Setter

In the setter of the dependency, we need to execute all functions that are watching this dependency (subscribers). In other words, use the notify() method we defined earlier.

set value(newValue) {
    this._value = newValue
    this.notify()
}
Enter fullscreen mode Exit fullscreen mode

Try it out

The implementation of dependency is done. Now it's time we try it out. To achieve that, we need to do 3 things:

  • Define a dependency
  • Add a function to be executed on dependency changes
  • Change the value of the dependency
// Create a reactive dependency with the value of 1
const count = new Dep(1)

// Add a "watcher". This logs every change of the dependency to the console.
watchEffect(() => {
    console.log('👻 value changed', count.value)
})

// Change value
setTimeout(() => {
    count.value++
}, 1000)
setTimeout(() => {
    count.value++
}, 2000)
setTimeout(() => {
    count.value++
}, 3000)
Enter fullscreen mode Exit fullscreen mode

In the console log you should be able to see something like this:

👻 value changed 1
👻 value changed 2
👻 value changed 3
👻 value changed 4
Enter fullscreen mode Exit fullscreen mode

You can find the complete code for the dependency on 👉 Github.

2. Building a reactive state

This is only the first part of the puzzle and mainly necessary to understand better what is going to happen next.

To recap: We have a reactive dependency and a watch function that together give us the possibility to execute a function whenever the variable (dependency) changes. Which is already pretty damn cool. But we want to go a step further and create a state.

Instead of somthing like this:

const count = Dep(1)
const name = Dep('Marc')
id.value = 2
name.value = 'Johnny'
Enter fullscreen mode Exit fullscreen mode

We want to do something like this:

const state = reactive({
    count: 1,
    name: 'Marc',
})
state.count = 2
state.name = 'Johnny'
Enter fullscreen mode Exit fullscreen mode

To achieve this, we need to make some changes to our code:

  • Add the reactive function. This created the "state" object.
  • Move getter and setter to the state instead of the dependency (because this is where the changes happen)

So the dependency (Dep) will only serve as such. Just the dependency part, not containing any value. The values are stored in the state.

The reactive function

The reactive() function can be seen as an initialization for the state. We pass an object to it with initial values, which is then converted to dependencies.

For each object property, the following must be done:

  • Define a dependency (Dep)
  • Definer getter
  • Define setter
function reactive(obj) {
    Object.keys(obj).forEach((key) => {
        const dep = new Dep()
        let value = obj[key]
        Object.defineProperty(obj, key, {
            get() {
                dep.depend()
                return value
            },
            set(newValue) {
                if (newValue !== value) {
                    value = newValue
                    dep.notify()
                }
            },
        })
    })
    return obj
}
Enter fullscreen mode Exit fullscreen mode

Changes on the dependency

Also, we need to remove the getter and setter from the dependency, since we do it now in the reactive state:

class Dep {
    subscribers = new Set()
    depend() {
        if (activeEffect) this.subscribers.add(activeEffect)
    }
    notify() {
        this.subscribers.forEach((sub) => sub())
    }
}
Enter fullscreen mode Exit fullscreen mode

The watchEffect function stays the same.

Try out the code

And we are already done with converting our dependency variable into a reactive state. Now we can try out the code:

const state = reactive({
    count: 1,
    name: 'Marc',
})

watchEffect(() => {
    console.log('👻 state changed', state.count, state.name)
})

setTimeout(() => {
    state.count++
    state.name = 'Johnny'
}, 1000)

setTimeout(() => {
    state.count++
}, 2000)
setTimeout(() => {
    state.count++
}, 3000)
Enter fullscreen mode Exit fullscreen mode

In the console log you should see something like this:

👻 state changed 1 Marc
👻 state changed 2 Marc
👻 state changed 2 Johnny
👻 state changed 3 Johnny
👻 state changed 4 Johnny
Enter fullscreen mode Exit fullscreen mode

You can find the complete code for the reactive state on 👉 Github.

Summary ✨

That's it for this part of the series. We did the following:

  • Create a dependency with a value inside, which notifies a subscribed function when the value changes
  • Create a state where a subscribed function is called for the change of every value

Original cover photo by Joshua Earle on Unplash, edited by Marc Backes.

. . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player