Reactivity in Vue.js


Here at XCentium our front-end developers often rely on Vue.js for prototyping and building out client sites. The framework has become our go-to as it’s fast, flexible, lightweight, and has a shallow learning curve.

One of the most powerful (and least understood) features of Vue.js is its reactivity system. This is what enables your HTML to automatically update and reflect changes in your data. In this post I’m going to break down exactly how this system works, so you can get a better understanding of what’s going on under the hood.

Before we begin, let’s define exactly what a “reactivity system” is. In a nutshell — it’s a system where variable X depends on the value of variable Y, and gets recalculated whenever any changes are made to variable Y.

Imagine an Excel spreadsheet. You can set cell A1 to 10, and set cell A2 to a function that doubles the value of A1. Then if you update A1 to 50, A2 becomes 100. The A2 function is dependent upon the current value of A1 in order to run. In other words, A1 is a “dependency” of A2.

Excel is aware of this dependency — so whenever A1 is updated — the A2 function is ran again. This is an example of a reactive system.

So how does this get implemented in Vue? Let’s create a simplified version of their reactivity system so we can see how it works.


Setup


First, let’s pretend we’re building a game. We’ll start by building a basic player object, and a function that will warn the player if their hit points (health) drops below 20:

const player = {
    name: 'Andrew',
    hp: 100,
    stamina: 100
}

function warning() {
    if (player.hp < 20) {
        console.log('You’re low on health!')
    }
}

As you can see from the above, the warning function is dependent upon player.hp. We also want it to be reactive, i.e. we want it run whenever player.hp is updated.


Detecting Changes


Next, let’s use the Object.defineProperty method to convert each property in our player object to getters and setters:

const player = {
    name: 'Andrew',
    hp: 100,
    stamina: 100
}

function warning() {
    if (player.hp < 20) {
        console.log('You’re low on health!')
    }
}

Object.entries(player).forEach(entry => {
    const [key, value] = entry
    let internalValue = value

    Object.defineProperty(player, key, {
        get() {
            return internalValue
        },

        set(newValue) {
            internalValue = newValue
        }
    })
})

With this change we can now detect whenever a player property is accessed (read from) or set (written to).


Tracking Dependencies


Whenever a player property is accessed — we want to register the currently running function as a dependency. Likewise, whenever a player property is set — we want to run all of the dependencies.

A simplified version of that process would look something like this:

const player = {
    name: 'Andrew',
    hp: 100,
    stamina: 100
}

function warning() {
    if (player.hp < 20) {
        console.log('You’re low on health!')
    }
}

Object.entries(player).forEach(entry => {
    const [key, value] = entry
    const dependencies = []

    let internalValue = value

    Object.defineProperty(player, key, {
        get() {
            dependencies.push(currentlyRunningFunc)
            return internalValue
        },

        set(newValue) {
            internalValue = newValue
            dependencies.forEach(func => func())
        }
    })
})

Which begs the question — how do we track and store the currently running function in the currentlyRunningFunc variable that you see above?


Tracking Function Executions


We can define a run function that will run a passed-in function. But before the passed-in function is ran, we set the currentlyRunningFunc variable to its value. For example:

let currentlyRunningFunc = null

function run(func) {
    currentlyRunningFunc = func
    func()
    currentlyRunningFunc = null
}

run(() => {})

Putting it all Together


If we combine all of the above, we end up with a working mini reactivity system!

We’ll also need to check if there’s a currently running function — and that it’s not already in the dependencies array — before adding it:

const player = {
    name: 'Andrew',
    hp: 100,
    stamina: 100
}

function warning() {
    if (player.hp < 20) {
        console.log('You’re low on health!')
    }
}

Object.entries(player).forEach(entry => {
    const [key, value] = entry
    const dependencies = []

    let internalValue = value

    Object.defineProperty(player, key, {
        get() {
            const exists = dependencies.includes(currentlyRunningFunc)

            if (currentlyRunningFunc && !exists) {
                dependencies.push(currentlyRunningFunc)
            }

            return internalValue
        },

        set(newValue) {
            internalValue = newValue
            dependencies.forEach(func => func())
        }
    })
})

let currentlyRunningFunc = null

function run(func) {
    currentlyRunningFunc = func
    func()
    currentlyRunningFunc = null
}

run(warning)

So let’s breakdown what’s happening here, step-by-step:

  1. warning is passed into run
  2. run stores warning in the currentlyRunningFunc variable
  3. run runs warning
  4. warning accesses player.hp (inside the if statement)
  5. This access triggers the set logic we defined for player.hp
  6. The set logic adds the warning function as a dependency
  7. run resets the currentlyRunningFunc variable to null

Giving it a Test Run


const monster = {
    type: 'Goblin',
    attack: 30
}

// the player is hit 3 times
player.hp = player.hp - monster.attack // 70 hp
player.hp = player.hp - monster.attack // 40 hp
player.hp = player.hp - monster.attack // 10 hp => "You’re low on health!"

As you can see — as soon as player.hp goes below 20, the console logs You’re low on health!. Magic! Or not, but nonetheless it’s still a clever pattern.


Additional Notes


  • This is a simplified version of the Vue reactivity system — the actual implementation is much more complex.
  • Since Vue runs Object.defineProperty on initialization — any dynamically added data properties won’t be reactive. To dynamically add data properties, use Vue.set.
  • DOM updates are asynchronous. So if you need to work with an updated DOM after updating a data property, you’ll need to use Vue.nextTick.

Sitecore Blogs:
View more blogs and tutorials about Sitecore.
Learn about our Sitecore work.
For thought leadership on Digital Strategy, please view these videos.

Categories: Front-End Development
Tags: JavaScript, Vue.js;

SEARCH ARTICLES