How Vue's reactivity works (I): Object.defineProperty
Maybe you don’t need to know how Vue reactivity works under the hood to make Vue apps, but anyway it will be interesting and useful.
In this context reactivity, means, simplifying, the capacity to detect a data change and do something after that.
In a Vue component, reactivity, means that the component will be re-rendered (totally or partially) after a change in the value of a variable to show the component updated with the new value. For example in this basic component:
<template>
<div>
<h6>Value: {{ clickCount }}</h6>
<button @click="onClick">Add 1 more</button>
</div>
</template>
<script>
export default {
data () {
return {
clickCount: 1
}
},
methods: {
onClick () {
this.clickCount = this.clickCount + 1
}
}
}
</script>
Every time you click on the “Add 1 more” button, the value of clickCount variable is increased in one unit and Vue starts the mechanism to re-render the component showing the new value in the template. How is Vue able to know when a variable changes its value?
Object.defineProperty API
The answer is Object.defineProperty
This is a static method that defines or modify a property on an object
const myObject = {}
Object.defineProperty(myObject, 'myProperty', {
value: 'myValue'
})
Probably you realized that is the same as myObject.myProperty = 'myValue'
, but there an important difference: we can configure the property behaviour, for example:
const myObject = {}
Object.defineProperty(myObject, 'myProperty', {
value: 'myValue',
writable: false
})
myObject.myProperty = 1
console.log(myObject.myProperty) // 'myValue'
In this situation, if you try to change the value of the property, it will not change, and if you are using strict mode you will get an exception.
With Object.defineProperty
you could define a getter and setter for the object property as shown in the following example. Every time you try to assign a value to your property getter is called.
const myObject = {}
let myProperyValue = 'my value'
Object.defineProperty(myObject, 'myProperty', {
get: () => {
console.log('getter')
return myProperyValue
},
set: (newValue) => {
console.log('setter')
myProperyValue = newValue
}
})
myObject.myProperty = 123
console.log(myObject.myProperty)
// setter
// getter
// 123
Note that if you use a getter or a setter you can’t access to the property’s value directly, I mean, you have to store property’s value somewhere else place.
Back in Vue, when you create a component you should define reactive values in data
key
export default {
data () {
return {
clickCount: 1
}
},
...
}
Under the hood, Vue creates an object with the properties you defined using Object.defineProperty and generates a getter and a setter. Every time a variable’s value changes, the setter intercepts the change and launches Vue’s re-render process with the new value.
This is the reason why you cannot add new variables to your component directly
export default {
data () {
return {
clickCount: 1
}
},
...
methods: {
someMethod () {
this.newClickCount = 1
...
}
}
...
}
In the example above, newClickCount
will not be reactive because Vue can’t know when you add a new property directly.
If you need to add a new property after the component’s definition Vue provides Vue.set
or vm.$set
this.$set(this.someObject, 'b', 2)
But this not work with the root element, I mean we cannot add a new variable to data
Object.defineProperty works since IE9, and in all modern browsers https://caniuse.com/?search=DefineProperty
How does it for arrays?
It doesn’t!. If you try to repeat the previous example with an array property:
const myObject = {}
let myProperyValue = []
Object.defineProperty(myObject, 'myProperty', {
get: () => {
console.log('getter')
return myProperyValue
},
set: (newValue) => {
console.log('setter')
myProperyValue = newValue
}
})
myObject.myProperty = [1, 2, 3, 4]
console.log(myObject.myProperty)
// setter
// getter
// [1, 2, 3, 4]
setter works because, is a direct assignation, but we usually don’t work with arrays in that way, we use .push
, slice
, etc..
myObject.myProperty.push(5)
console.log(myObject.myProperty)
// getter
// getter
// [1, 2, 3, 4, 5]
We can see getter has been called twice, but the setter has not been called
How does Vue resolve it?
Simple, patching vanilla JS array methods
Vue stores the original method, and create a new that notifies the change and execute the original method
You can see how it does in detail on: https://github.com/vuejs/vue/blob/bb253db0b3e17124b6d1fe93fbf2db35470a1347/packages/vue-template-compiler/build.js#L1087
ES6 Proxies
There is another way to know when a value change (and other things) in JS since ES6: Proxies
Vue 3 uses Proxies instead of Object.defineProperty to make the reactivity under the hood. I will write a post about Proxies soon.