Reactivity is the ability to track changes and perform actions based on said changed
In Vue.js, the component state is reactive
The component state includes the "data", "computed", and "props" properties.
Example on reactivity in Vue.js
<template>
<div>
<span>{{ displayName }}</span>
</div>
</template>
<script>
export default {
props: {
title: {
type: String
}
},
data: () => ({
firstName: '',
lastName: ''
}),
computed: {
// How does Vue know that any of these properties changed?
displayName () {
return `${this.title}. ${this.firstName} ${this.lastName}`;
}
}
};
</script>
let x = 10;
function double () {
return x * 2;
}
double(); // 20
x = 5;
We would need to keep calling 'double' to get the new value
let x = 10;
let state = {};
Object.defineProperty(state, 'doubleX', {
get () {
return x * 2;
}
})
state.doubleX; // 20
x = 5;
state.doubleX; // 10
Now we have a 'live' reference that returns the new value every time
This is not enough, we still need to 'get' the value.
We merely changed the style.
// You can pass a function to the vm.$watch
// How does Vue know that it uses firstName and lastName?
this.$watch(() => {
return `${this.firstName} ${this.lastName}`;
}, (val, oldVal) => {
console.log(val, oldVal);
});
It is not magic, it is still JavaScript.
You can watch a function expression with Vue.js watch API
An object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes.
1. We need to observe objects
const state = {
x: 10
};
function observe (obj) {
// ???
}
const observableState = observe(state);
2. We need to watch for changes
const double = () => {
return state.x * 2;
};
function watch (exp, callback) {
// ???
}
watch(double, val => {
console.log(val);
});
// Takes an object and returns an observable one.
function observe (obj) {
const observable = {};
Object.keys(obj).forEach(key => {
// Set the initial value under a private key.
observable[`_${key}`] = { value: obj[key] };
// Expose getters/setters to proxy the underlying value.
Object.defineProperty(observable, key, {
get () {
return this[`_${key}`].value;
},
set (val) {
this[`_${key}`].value = val;
}
});
});
return observable;
}
Something is still missing 🤔
function watch (expression, callback) {
// Do something with the expression.
// Run the callback once the expression dependencies change.
}
We hit a roadblock, just by executing the expression we have to be able to tell which properties did it use!
We can call these expressions: observers.
We need to "collect" those observers and have the subject notify them.
// Simple function to wrap values in.
function wrap (value) {
return {
value,
deps: [],
notify () {
// runs the observers
this.deps.forEach(dep => dep());
}
};
}
We need to wrap values as subjects and have them notify their observers.
// ...
Object.keys(obj).forEach(key => {
// Set the initial value under a private key.
observable[`_${key}`] = wrap(obj[key]);
// Expose getters/setters to proxy the underlying value.
Object.defineProperty(observable, key, {
get () {
const subject = this[`_${key}`];
// collecting dependents
subject.deps.push(
// 😕 ???
);
return subject.value;
},
set (val) {
const subject = this[`_${key}`];
subject.value = val;
// notify observers.
subject.notify();
}
});
});
// ...
The key is relying on an important but often ignored aspect of JavaScript ...
JavaScript is single-threaded, which means there can only be 1 line of code running in any given moment...
By extension, There can only be 1 function running in any given moment.
🥁🥁🥁🥁
// I want this to be hoisted.
var activeFn;
// watches an expression and runs the callback.
function watch (expression, callback) {
activeFn = expression;
expression();
activeFn = undefined;
}
// ...
Object.defineProperty(observable, key, {
get () {
const subject = this[`_${key}`];
// collecting dependents
if (activeFn) {
subject.deps.push(
activeFn
);
}
return subject.value;
},
set (val) {
const subject = this[`_${key}`];
subject.value = val;
subject.notify();
}
});
//...
const state = observe({
x: 10
});
watch(() => {
console.log('I ran!');
return state.x * 2;
}, () => {});
state.x = 1;
state.x = 2;
state.x = 3;
// we should see "I ran 4x times"
// watches an expression and runs the callback.
function watch (expression, callback) {
activeFn = expression;
// not a very good way to do it, but 🤷
expression.fn = callback;
// by running the expression, we ran our `get` handlers.
expression();
activeFn = undefined; // must be done!
}
function wrap (value) {
return {
// ...
notify () {
// in our set handler
obs.deps.forEach(dep => {
const newVal = dep();
if (dep.fn) {
// pass the new value to the attached handler.
dep.fn(newVal);
}
});
}
}
}
// state object
const state = observe({
count: 0
});
// render-fn
const render = () => {
return `
<h1>
Count is: ${state.count}
</h1>
`;
};
// start watching.
watch(render, (value) => {
document.body.innerHTML = value;
});
// run the first render
document.body.innerHTML = render();
setInterval(() => {
state.count++;
}, 500);
We can make a watcher become a computed prop
by adding state, we will need a wrapper for that.
function makeComputed (exp) {
const wrapper = observe({
value: exp()
});
watch(exp, val => {
wrapper.value = val;
});
return wrapper;
};
const state = observe({
x: 10
});
const double = makeComputed(() => state.x * 2);
1. Duplicate Dependencies
const state = observe({
firstName: '',
lastName: ''
});
const exp = () => {
state.firstName;
state.firstName;
state.firstName;
};
// state.firstName will be collected 3 times !!!
watch(exp);
1. Duplicate Dependencies: Solution
// ...
// ...
// Set the initial value under a private key.
observable[`_${key}`] = {
value: obj[key],
deps: new Set(), // instead of array
notify () {
// ...
}
};
// ...
// ...
// ...
// In the gettter
if (activeFn) {
subject.deps.add(activeFn); // instead of push
}
2. Objects
const state = observe({
user: {
firstName: '',
lastName: '',
social: {
fb: '....',
tt: '....'
}
}
});
// Can be observed!
state.user.firstName = 'Amr';
let user = state.user;
// Will not be observed!
user.firstName = 'Abdelrahman';
// What about this?
user.social.fb = '....';
2. Objects: Solution
Object.keys(obj).forEach(key => {
let prop = obj[key];
// because typeof null = 'object' (╯°□°)╯︵ ┻━┻
// typeof [] = 'object'
if (typeof prop === 'object' && prop && !Array.isArray(prop)) {
// recursive.
prop = observe(prop);
}
observable[`_${key}`] = wrap(prop);
// ...
// ...
}
3. Arrays
const state = observe({
items: []
});
// Should observe appended items!
state.items.push({ value: 0 });
// Should observe properties on each items.
state.items[0].value = 10;
// Should observe node removals.
state.items.splice(0, 1);
3. Arrays: Solution
Object.keys(obj).forEach(key => {
let prop = obj[key];
if (Array.isArray(prop)) {
// convert all items to observables first!
prop = prop.map(observe);
obj.push = function pushReactive (...args) {
// convert all added items to reactive items.
let reactiveItems = args.map(observe);
const result = Array.prototype.push.call(prop, ...reactiveItems);
observable[`_${key}`].notify(); // notify dependencies
return result;
}
obj.splice = function spliceReactive (...args) {
let result = Array.prototype.splice.call(prop, ...args);
observable[`_${key}`].notify(); // notify dependencies
return result;
}
}
// ...
});
const state = observe({
isLoading: true,
loadedMessage: 'Hello',
loadingMessage: 'Loading...',
user: {
name: ''
},
items: ['coffee', 'tea', 'soda']
});
const stale = () => {
if (state.isLoading) {
// this will become stale and will never used again
// this is called 'stale dependencies'
return state.loadingMessage;
}
return state.loadedMessage;
}
// we cannot detect array prop changes
// only using 'push' and 'splice'
state.items[0] = 'water';
// adding props won't work
// we need to have properties predefined.
state.user.age = 19;
Will use ES6 proxies instead of ES5 getters/setters.
Will be able to handle arrays better.
Will be able to handle newly added properties.
Half the memory usage and double the speed.
<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>