Closed domenic closed 1 year ago
The context around the statement, step by step:
// An observable-ish function
function ticker(observer) {
let n = 0;
const id = setInterval(() => {
observer.next(n++)
if (n === 5) {
observer.complete();
// developer mistake here
observer.next('oops')
}
}, 1000);
return () => {
clearInterval(id);
}
}
// usage
const unsub = ticker({
next: console.log,
complete: () => console.log('done')
})
// unsubscribing later (optional)
setTimeout(() => {
unsub();
}, 60000);
This seems fine, but unfortunately there are a lot of bugs that aren't immediately obvious to the developer.
observer.next()
after observer.complete()
. That shouldn't do anything, and it will.To resolve these issues, we have to take the SAME function and wrap it in something that will internally provide these guarantees:
// It's the same function, but we've wrapped it in a class
// that will take the passed observer and wrap it in a "subscriber"
// that does a few things:
// 1. Ensures safety around calling `next`, `complete`, or `error` after everything is finalized.
// 2. Links the subscription teardown to the calling of `complete` or `error` to guarantee cleanup.
// 3. Allows partial observers to be passed (each handler is optional).
const ticker = new Observable((subscriber) => {
let n = 0;
const id = setInterval(() => {
subscriber.next(n++)
if (n === 5) {
subscriber.complete();
// developer mistake here
subscriber.next('oops')
}
}, 1000);
return () => {
clearInterval(id);
}
});
// usage
// Subscribe just immediately calls the function with the
// subscriber that wraps the passed observer.
const subscription = ticker.subscribe({
next: console.log,
complete: () => console.log('done')
})
// unsubscribing later (optional)
setTimeout(() => {
subscription.unsubscribe();
}, 60000);
that's really it in a nutshell.
(I'm NOT proposing this, just stating it for perspective...) If we created some JS-language level syntax for this, it could even look something like a generator function. function#
or the like. It's just instead of returning a generator/iterator, you'd get an observable.
Maybe there is some abstract sense in which this is true, but it makes no sense to me. The natural followup questions with this framing are:
Can it be constructed, or just called?
Is it an async function, a generator function, a plain function...?
Why do we need these when we already have functions?
And then
just makes it seem like some of the most important features of observables, don't even fit into this model?
I would suggest describing how observables work, independent of any analogy with functions. IIUC the main points to communicate are:
The creator of the observable can signal zero or more values, followed by either one "complete" signal or one "error" signal. The error signal comes with an error value, which is traditionally a subclass of
Error
.The creator's code to generate this sequence-of-values is called lazily, meaning, it happens once
subscribe()
is called.The creator's code to generate this sequence of values is called potentially multiple times, whenever
subscribe()
is called.I would then very quickly move into making this concrete via events, possibly with code examples. I.e., you can call
subscribe()
as many times as you want, and every time, it calls "the creator's code" which subscribes to theEventTarget
. (Using the same mechanism, under the hood, asaddEventListener()
does.) This makes it more clear why the above properties are desirable properties, for practical purposes. (Although you need to do a bit more work to explain the complete and error signals.)