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?

Made with Slides.com