Notes on Observables.
/**
* A simple Observable class with `map` and `filter` operators.
*
* When instantiated, Observable classes don't do anything until they
* are "subscribed" to.
*
* The constructor takes an `observableFn` param that's a function. The passed
* observable function is the actual thing we're observing and the Observable
* class is just a wrapper for this observable function. Therefore we can say
* that observables are "just functions".
*/
class Observable {
/**
* subscribe() is the function that's being observed.
*
* subscribe() accepts an `observer` param, which is an object with a
* next(), error(), and a complete() method.
*/
constructor(observableFn) {
this.subscribe = observableFn; // rename `observableFn` to `subscribe`
}
/**
* Operators are just functions that return a new observable.
*
* When its own `observableFn` is called, it calls the `prevObservableFn`
* passing in an observer object in order to first "map" the values return
* from `prevObservableFn`.
*/
map(mapFn) {
const prevObservableFn = this.subscribe;
// This new observable acts as a wrapper of the previous observable.
return new Observable(observer => {
// Here we're "subscribing" the operator's observable to the outer observable.
const unsubscribe = prevObservableFn({
next(val) {
const mappedValue = mapFn(val);
observer.next(mappedValue); // call the operator's observer after mapping the value
},
error: observer.error,
complete: observer.complete
});
// Whatever the outer observable returns as an `unsubscribe`
return unsubscribe;
});
}
filter(condition) {
const prevObservableFn = this.subscribe;
return new Observable((observer) => {
const unsubscribe = prevObservableFn({
next(val) {
if (condition(val)) {
observer.next(val);
}
},
error: observer.error,
complete: observer.complete,
});
return unsubscribe;
});
}
}
An interval observable with two subscriptions.
Live example: https://codepen.io/anon/pen/drxJJb?editors=0010
const intervalObservable = new Observable(observer => {
let count = 0;
const id = setInterval(() => {
observer.next(++count);
if (count === 3) {
observer.complete();
}
}, 1200);
// Provide a way of canceling the interval
return function unsubscribe() {
clearInterval(id);
};
});
// (First subscription): calling map returns a new Observable
const mapObservable = intervalObservable.map(val => val * 10);
const unsubscribe = mapObservable.subscribe({
next(val) {
console.log('First:', val);
},
complete() {
console.log('First:', 'Done!');
unsubscribe();
}
});
// (Second subscription): calling map returns a new Observable
const unsubscribeSecond = intervalObservable
.map(val => val * 20)
.subscribe({
next(val) {
console.log('Second:', val);
},
complete() {
console.log('Second:', 'Done!');
unsubscribeSecond();
}
});
A DOM event observable.
Live example: https://codepen.io/vasilionjea/pen/MWRBzvd?editors=0010
function fromEvent(element, eventName) {
return new Observable((observer) => {
const handler = (e) => observer.next(e);
element.addEventListener(eventName, handler);
return () => {
element.removeEventListener(eventName, handler);
};
});
}
const textarea = document.querySelector('textarea');
const output = document.querySelector('output');
fromEvent(textarea, 'keyup')
.map((e) => e.key.toUpperCase())
.subscribe({
next(key) {
output.textContent += `${key} `;
}
});