Reactivity
How does reactivity work?
- Prelude;
- Map;
- Track;
- Trigger;
- Reactive;
- Conclusion.
Agenda
Referencies
Prelude
Vue
One of Vue’s most distinctive features is the unobtrusive reactivity system. Component state consists of reactive JavaScript objects. When you modify them, the view updates. It makes state management simple and intuitive, but it’s also important to understand how it works to avoid some common gotchas. In this section, we are going to dig into some of the lower-level details of Vue’s reactivity system.
What is Reactivity?
This term comes up in programming quite a bit these days, but what do people mean when they say it? Reactivity is a programming paradigm that allows us to adjust to changes in a declarative manner. The canonical example that people usually show, because it’s a great one, is an Excel spreadsheet.
Example:
A | B | C | |
---|---|---|---|
0 | 1 | ||
1 | 2 | ||
2 | 3 |
Here cell A2 is defined via a formula of =
A0 + A1 (you can click on A2 to view or edit the formula), so the spreadsheet gives us 3. No surprises there. But if you update A0 or A1, you'll notice that A2 automagically updates too.
JavaScript doesn’t usually work like this. If we were to write something comparable in JavaScript:
Problem
let A0 = 1
let A1 = 2
let A2 = A0 + A1
console.log(A2) // 3
A0 = 2
console.log(A2) // Still 3
So how would we do this in JavaScript? First, in order to re-run the code that updates A2
, let's wrap it in a function:
Solution
let A2
function update() {
A2 = A0 + A1
}
-
A0 and A1 are considered dependencies of the effect, as their values are used to perform the effect. The effect is said to be a subscriber to its dependencies.
-
The update() function produces a side effect, or effect for short, because it modifies the state of the program.
Then, we need to define a few terms:
Result
What we need is a magic function that can invoke update() (the effect) whenever A0 or A1 (the dependencies) change:
whenDepsChange(update)
-
Track when a variable is read. E.g. when evaluating the expression A0 + A1, both A0 and A1 are read;
- if a variable is read when there is a currently running effect, make that effect a subscriber to that variable. E.g. because A0 and A1 are read when update() is being executed, update() becomes a subscriber to both A0 and A1 after the first call.
-
Detect when a variable is mutated. E.g. when
A0
is assigned a new value, notify all its subscriber effects to re-run.
This whenDepsChange() function has the following tasks:
We can't really track the reading and writing of local variables like in the example. There's just no mechanism for doing that in vanilla JavaScript. What we can do though, is intercept the reading and writing of object properties.
There are two ways of intercepting property access in JavaScript: getter/setters and Proxies. Vue 2 used getter/setters exclusively due to browser support limitations. In Vue 3, Proxies are used for reactive objects and getter/setters are used for refs.
How Reactivity Works in Vue
Map
First step what we need to create a Map structure
// Component's dependencies entry point, where all dependencies are allocated
const dependencies = new Map()
Map or WeakMap
Under the hood of the Vue framework the Map
structure is used for a component dependencies itself, while WeakMap is used for whole Vue's components.
Track
Track
The track function will registrate your effects into a data's field which is defined.
Second step what we need to create the track function
function track (target, effects) {
let dependency = dependencies.get(target)
if (!dependency) {
dependencies.set(target, (dependency = new Set()))
}
effects.forEach((effect) => { dependency.add(effect) })
}
Set
The Set
object lets you store unique values of any type, whether primitive values or object references.
Trigger
Trigger
The trigger function will invoke all effects (which you difine into computed's object) when you set a new value to object's property (which is called data).
Third step what we need to create the trigger function
function trigger (target) {
const dependency = dependencies.get(target)
if (!dependency) { return }
dependency.forEach((effect) => {
target[effect.name] = effect.call(target)
})
}
Reactive
Reactive
The Proxy pattern which will track and trigger the effects by using get and set trap.
Fourth step what we need to create the reactive function
function reactive (obj, effects) {
return new Proxy(obj, {
get (target, key) {
track(target, effects)
return Reflect.get(target, key)
},
set (target, key, value) {
Reflect.set(target, key, value)
trigger(target)
},
})
}
Conclusion
That's it?
Not really we have to define some helpers to merge this functions
Merge computed and data between each other
function merge ({ data, computed }) {
const target = Object.values(computed).reduce((result, fn) => Object.assign(result, {
[fn.name]: fn.call({ ...data, ...result })
}), {})
return Object.assign(data, target)
}
Init our reactivity system
function init (obj) {
Object.keys(obj).forEach(key => obj[key])
}
Cue (Vue) framework
function Cue (obj) {
const target = merge(obj)
const effects = Object.values(obj.computed)
const result = reactive(target, effects)
init(result)
return result
}
Final code
Final code
((environment) => {
const dependencies = new Map()
function track (target, effects) {
let dependency = dependencies.get(target)
if (!dependency) {
dependencies.set(target, (dependency = new Set()))
}
effects.forEach((effect) => { dependency.add(effect) })
console.log('after track -> dependency', dependency)
}
function trigger (target) {
const dependency = dependencies.get(target)
if (!dependency) { return }
console.log('before trigger -> dependency', dependency)
console.log('before trigger -> target', target)
dependency.forEach((effect) => {
target[effect.name] = effect.call(target)
})
console.log('after trigger -> target', target)
}
function reactive (obj, effects) {
return new Proxy(obj, {
get (target, key) {
console.log('reactive -> get -> target', target)
track(target, effects)
return Reflect.get(target, key)
},
set (target, key, value) {
console.log('reactive -> set -> target', target)
Reflect.set(target, key, value)
trigger(target)
}
})
}
function init (obj) {
Object.keys(obj).forEach(key => obj[key])
}
function merge ({ data, computed }) {
const target = Object.values(computed).reduce((result ,fn) => Object.assign(result, {
[fn.name]: fn.call({ ...data, ...result })
}), {})
console.log('merge -> target', target)
return Object.assign(data, target)
}
function Cue (obj) {
const target = merge(obj)
const effects = Object.values(obj.computed)
const result = reactive(target, effects)
init(result)
return result
}
environment.Cue = Cue
})(window)
var cue = Cue({
data: {
a: 2
},
computed: {
getA () { return this.a * 2 },
getB () { return this.getA * 10 }
}
})
console.log(cue)
Question
What if we change a computed property, will be this property changed?
Thank You!
Questions?
Reactivity
By Cyril Mialik
Reactivity
- 32