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:
warning
- is passed into
run
run
- stores
warning
- in the
currentlyRunningFunc
- variable
run
- runs
warning
warning
- accesses
player.hp
- (inside the if statement)
- This access triggers the
set
- logic we defined for
player.hp
- The
set
- logic adds the
warning
- function as a dependency
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.