xc banner image

Reactivity in Vue

Senior FrontEnd Developer
  • Twitter
  • LinkedIn


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) drop 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 us

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 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
  2. is passed into
  3. run
  4. run
  5. stores
  6. warning
  7. in the
  8. currentlyRunningFunc
  9. variable
  10. run
  11. runs
  12. warning
  13. warning
  14. accesses
  15. player.hp
  16. (inside the if statement)
  17. This access triggers the
  18. set
  19. logic we defined for
  20. player.hp
  21. The
  22. set
  23. logic adds the
  24. warning
  25. function as a dependency
  26. run
  27. resets the
  28. currentlyRunningFunc
  29. 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.