Abdelrahman Awad
Software Engineer @Rasayel. Open-Source contributor.
const array = [2, 1, 4, -1, 5];
const sorted = array.sort();
console.log(sorted === array) // yep
const str = 'hello world';
const newStr = str.replace('he', 'a');
console.log(str === newStr); // Nope!
const array = [2, 1, 4, -1, 5];
function sort (arr) {
return [...arr].sort();
}
const sorted = sort(array);
console.log(sorted === array); // Nope!
// Predicability.
const arr = [1, 2, 3];
arr.sort(); // unintended side-effect.
// Performance
function pushItem(arr, item) {
let newArr = [...arr].push(item);
return newArr;
}
// assume we do this 1000 times
const arr = [];
pushItem(arr, 1);
const [state, setState] = useState({});
setState({ a: 1 }); // Immutable.
this.state = { a: 1 };
this.state.a = 3; // Mutable
this.state.b = 4; // ??
Check everything for anything that has changed
AngularJS: Doesn't track
Angular 2+ Doesn't dirty check, at least not all the time and not everything.
Doesn't dirty check, but doesn't track either.
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
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.
🥁🥁🥁🥁
let activeFn;
// watches an expression and runs the callback.
function watch (expression, callback) {
activeFn = expression;
expression();
activeFn = undefined;
}
// 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);
In Pull systems, the Consumer determines when it receives data from the data Producer. The Producer itself is unaware of when the data will be delivered to the Consumer.
Producer:
Passive
(produces data when requested)
Consumer:
Active
(decides when data is requested)
// Produce!
function doSomething () {
// ...
}
// Consume!
doSomething();
Credits: Evan You - Reactivity in Frontend JavaScript Frameworks
In Push systems, the Producer determines when to send data to the Consumer. The Consumer is unaware of when it will receive that data.
Producer:
Passive
(produces data on its own)
Consumer:
Active
(Reacts to produced data)
const el = document.querySelector('#el');
// Produce? Consume?
el.addEventListener(function () {
// ...
});
Credits: Evan You - Reactivity in Frontend JavaScript Frameworks
react.js has nothing to do with reactivity as it doesn't react to anything.
Should name it `tell-me-when-to-render.js`
These are called "performance hints"
Uses a concept called `zones` to wrap web APIs to simulate reactivity
function addEventListener(eventName, callback) {
// call the real addEventListener
callRealAddEventListener(eventName, function() {
// first call the original callback
callback(...);
// and then run Angular-specific functionality
var changed = angular.runChangeDetection();
if (changed) {
angular.reRenderUIPart();
}
});
}
Angular wraps every almost Every API in JavaScript to simulate reactivity to then dirty check.
Then it only checks which template-referenced parts did change, it is efficient but requires template compilation.
Luckily with Angular when you need to fine-tune such performance bottlenecks if observed, you can change the detection strategy from pull to push using `onPush`
For Vue.js it knows what changed, what components depend on that change and thus is able to update each component independently without having to re-render their children nor needing any performance hints.
Thanks 👋
By Abdelrahman Awad