I have been wrapping up new ideas got from Vue London since last week. I’m really happy to see the community of Vue has grown to such a scale.

There is an interesting speech about the reactivity inside Vue framework that I think is worth sharing. And, I have done my own implementation with ES5 standard defineProperty and ES6 standard Proxy class.

Here is the git repo:
https://github.com/krnan9525/js-reactivity

Also, some references:
Vue Doc - reactivity in depth,
Hackerroom - How to Build Your Own Reactivity System

_So, what is reactivity and why do we care? _

Well, the simple way to understand it is through code examples.
Here is some simple Javascript code:

1
2
3
4
5
let a = 1, b = 2;
let result = a + b;
// result = 3
a = 2;
// result = 3, because variable a and b doesn't have 'reactivity'

In a lot of scenarios, we would expect some variables’ values changing while the value of their dependency changes. This is especially beneficial in the frontend universe – so we can avoid the whole page being reloaded every time there is a change and a part of the page needs to be re-rendered.

In the following paragraphs, I will briefly walk through the implementation side of this idea, the benefits and the overhead. And since this blog is mainly about JS, I would assume you all have the prior knowledge of basic Javascript and ES6.

First, git clone https://github.com/krnan9525/js-reactivity.

As I mentioned before, there are a few ways to achieve reactivity. In Vue versions lower than 3.0 or 2.6-next, it’s achieved by calling defineProperty function, otherwise, by using Proxy. This also means there will be no native support for IE 11 in Vue 3.0 (you can still polyfill it but some functionality will be missing down the line).
There are several reasons for this switching, biggest two will be supporting addition and deletion of properties and not modifying the original object. You can refer to the Hackerroom link above for more details.

Underlining Workflow for Achieving Reactivity
Vue reactivity diagram
You can find this picture on Vue website, explaining how is done in Vue.
Let’s only focus on Watcher and Data. Note, the Watcher here is different
than what watch property is inside a Vue component.

  • Data

    • only the data that deciding other’s value needs to be reactive. In the example above, variable a and b need to be but result doesn’t.
    • setter and getter methods are called when access / assign value to a variable.
    • getter function will add dependency to the current ‘reactive’ data object.
    • setter function will change the value of dependencies when the current value changed.
  • Watcher

    • As the name indicated, watchers are used to tell the reactive system that variable A is depending on variable B. So that whenever B got updated, a function should be called to automatically update the value of A.

You might have noticed, this whole workflow is based on subscribe to dependencies and notify dependencies when it needs to be updated. To bring this to programming language, we are going to introduce a class called Dep as a property of the reactivity object.

  • Dep class
    • this class contains methods subscribe and notify reflecting the two outbound arrows from the data object on the graph. It also has an array property subscribers which is simply the collection of dependencies gathered by subscribe method.

Now, let’s do a deep dive into the code.

Coding in Action

  1. implementation with ES5 style defineProperty method (In file @/src/reactive)

Consumer Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { beReactive, setWatcher } = require('./lib/reactive')

let product = {
beer: {
price: 20,
amount: 3
}
}

beReactive(product)

let totalPrice = {}

setWatcher(() => {totalPrice.beer = product.beer.amount * product.beer.price})

The code above is very straight-forward,

  • -> define variable A
  • -> make variable A reactive
  • -> define variable B, which is depending on A
  • -> set a watcher for variable B

Implantation (lib) Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
let _target = null

class _Dep {
constructor () {
this.subscribers = []
}
depend () {
if (_target && !this.subscribers.includes(_target)) {
this.subscribers.push(_target)
}
}
notify () {
this.subscribers.forEach(sub => sub())
}
}

const beReactive = (vals, index = undefined) => {
 // call _setRecativityForValue function recursively for all properties of 'vals'
}

const _setReactivityForValue = (val, index) => {
let _value = val[index]
const dep = new _Dep()
Object.defineProperty(val, index, {
set (newVal) {
_value = newVal
dep.notify()
},
get () {
dep.depend()
return _value
}
})
}

const setWatcher = (func) => {
_target = func
_target()
_target = null
}

module.exports = { beReactive, setWatcher }

This code should be clear to read, just a few things to note:

  • for watchers, we accept a function as a parameter to construct the relation between multiple variables. It will first set the internal variable _target to the target function then run it, and in the meantime, the getter function on all reactive properties will be called and their depend() function will be called to push the target function into their subscriber array.
  • when a new value is set, the setter function will be called and all the depending variables will be updated.
  1. implementation with ES6 style Proxy Class (In file @/src/reactiveWithProxy)

There is only one different on consumer side. As I mentioned before, by lifting ES6 Proxy class, we have the ability not changing the original value. So that, consumer need to assign the value resolved by the proxy to a variable.

Consumer Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const { beReactive, setWatcher } = require('./lib/reactiveWithProxy')

let product = {
beer: {
price: 20,
amount: 3
}
}

const productReactive = beReactive(product)

let totalPrice = {}

setWatcher(() => {totalPrice.beer = productReactive.beer.amount * productReactive.beer.price})

console.log(totalPrice)

productReactive.beer.amount = 10

console.log(totalPrice)

productReactive.tea = {amount: 10, price: 10}

setWatcher(() => {totalPrice.tea = productReactive.tea.amount * productReactive.tea.price})

console.log(totalPrice)

productReactive.tea.amount = 100

console.log(totalPrice)

The above code follows the same workflow as the defineProperty version consumer code, except now you can add reactive property after the object is defined. And this is a huge improvement compare with ES5 defineProperty function.

Implantation (lib) Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
let _target = null

// same as the previous example
class _Dep {
constructor () {
this._subscribers = []
}
depend () {
if (_target && !this._subscribers.includes(_target)) {
this._subscribers.push(_target)
}
}
notify () {
this._subscribers.forEach(sub => sub())
}
}

// function entry for achieving reactivity
const beReactive = (vals) => {
return _setReactivityForValue(vals)
}

// same as the previous example
const setWatcher = (func) => {
_target = func
_target()
_target = null
}

// not all the deps for every object are stored in one object
const _constructDeps = (vals) => {
......// please refer to the code on Github
}

// recursively setting reactivity for all properties of an object, as well as the added properties after the object is defined.
const _setReactivityForValue = (val) => {
let _deps = _constructDeps(val)

let validator = (varPath = []) => {
return {
get (obj, key) {
let _path = varPath.slice()
......// please refer to the code on Github
_currentDep[key].depend()
return obj[key]
},
set (obj, key, newVal) {
......// please refer to the code on Github
obj[key] = newVal
_currentDep[key].notify()
return true
}
}
}
return new Proxy(val, validator([]))
}

module.exports = { beReactive, setWatcher }

Console Output

upload successful

Todo: Using reactivity in Vue, with code examples