ponyfoo.com

Observables Proposal for ECMAScript!

There’s an ECMAScript proposal for Observables in the works. In this article we explore the proposal, the API, and look at a few use cases.

Improve this article
Nicolás Bevacqua
| 6 minute read | 11

Observables in JavaScript were largely popularized by libraries such as RxJS and Bacon.js. Jafar Husain, a Netflix tech lead and long-time functional programming advocate who’s also on TC39 has been developing a proposal to bring observables into the core language. The Observable proposal is at stage 1 but marked as ready to move up to stage 2, at the time of this writing.

Observable and the observer API

In this proposal, Observable would be a new built-in that can be used to handle event streams. The Observable constructor takes a callback which defines an event stream. In the following example, our observable returns a stream of events with just 1 and 2 values. The observer.next method can be used to add events to the observable stream.

new Observable(observer => {
  observer.next(1)
  observer.next(2)
})

We can use observer.error to report errors that occur during stream processing.

new Observable(observer => {
  observer.error(new Error(`Failed to stream events`))
})

We can use observer.complete to signal when the event stream has come to an end.

new Observable(observer => {
  observer.next(1)
  observer.next(2)
  observer.complete()
})

The callback passed to an Observable constructor can return a cleanup function to tear down our observable. This can be useful to remove event listeners, clear timeouts, and similar cleanup tasks. The following observable is a bit more interesting than the last one. It tracks mouse position relative to the page as the user moves the cursor on screen, producing an event stream that describes the cursor position on the page through time.

function mouseTracking () {
  return new Observable(observer => {
    const handler = ({ pageX, pageY }) => {
      observer.next({ x: pageX, y: pageY })
    }

    document.body.addEventListener(`mousemove`, handler)

    return () =>  {
      document.body.removeEventListener(`mousemove`, handler)
    }
  })
}

In order for us to subscribe to an observable event stream, we just call the Observable#subscribe method on an observable object instance. Doing so will invoke the callback passed to the Observable constructor in the previous code snippet, binding the event listener and getting the event stream started. Moving the mouse will now result in events being fired into the event stream.

mouseTracking().subscribe({
  next({ x, y }) { console.log(`New position: ${ x }, ${ y }`) },
  error(err) { console.log(`Error: ${ err }`) },
  complete() { console.log(`Done!`) }
})

Subscription#unsubscribe

Observable#subscribe returns an object that lets us unsubscribe, executing the cleanup method – if one exists. When we’re no longer interested in events from the observable stream, we’ll unsubscribe and let the observable clean itself up.

const subscription = mouseTracking().subscribe({
  next({ x, y }) { console.log(`New position: ${ x }, ${ y }`) },
  error(err) { console.log(`Error: ${ err }`) },
  complete() { console.log(`Done!`) }
})

subscription.unsubscribe()

Observable.of

Observable.of(...items) is a simple static utility helper that creates an Observable out of the provided items. The items are then delivered synchronously when Observable#subscribe is called.

Observable.of(1, 2, 3, 4).subscribe({
  next(item) { console.log(item) }
})
// <- 1
// <- 2
// <- 3
// <- 4

We can think of Observable.of as the following simplified implementation, where we return a synchronous stream of provided values.

Observable.of = (...items) => {
  return new Observable(observer => {
    items.forEach(item => {
      observer.next(item)
    })
    observer.complete()
  })
}

Observable.from

This static method casts the provided argument into an Observable. If the provided Object has a Symbol.observable method, then the result of invoking that method is returned.

Observable
  .from({
    [Symbol.observable]() { return Observable.of(1, 2, 3) }
  })
  .subscribe({
    next(item) { console.log(item) }
  })
// <- 1
// <- 2
// <- 3

If the provided argument doesn’t implement a Symbol.observable method, then it’s assumed to be an iterable. A synchronous Observable sequence of the iterable is returned.

Observable
  .from([1, 2, 3])
  .subscribe({
    next(item) { console.log(item) }
  })
// <- 1
// <- 2
// <- 3

In this case, Observable.from is similar to Observable.of. We could think of Observable.from as the following simplified implementation.

Observable.from = value => {
  if (typeof value[Symbol.observable] === `function`) {
    return value[Symbol.observable]()
  }
  return Observable.of(Array.from(value))
}

Conclusions

Note that this proposal is still in its infancy, but it’d lay out the foundation for functional programming at the native JavaScript level. Eventually, it may earn the ability to Observable#filter or Observable#map the stream of events, allowing us to focus only on the kinds of events we want to listen for.

In the meantime, these could be implemented in user-land as we continue to watch out for patterns and let the specification evolve naturally and gradually. You can find a polyfill for the current incarnation of the specification on the GitHub repository, but you’ll have to delete the export keyword if you want to play around with it in your browser’s Dev Tools.

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments (11)

Jethro Larson wrote

Good callout on map and filter I really think those are crucial for a usable implementation.

Damien Lebrun wrote

It’s useable without more methods. By having native Observable, builtin methods can return Observables: instead of converting DOM events to Observable, you can use a native DOM method returning an Observable.

You need to import a third party library to transform or compose observable, but you won’t have to bridge different style of event emitter, iterator, async generator, etc… Also those third party libraries will be able to use the same specs and work with each other.

Brook Monroe wrote

Just for fun, clarify for me how this is different from Object.observe(), which was deprecated. I thought proxies were taking over this responsibility.

Andreas Linnert wrote

Observable can do a lot more. You can use Proxies and Observables to implement your own Object.observe(). Observables on the other hand can notify you about everything: any kind of event, timers, random events. Another use case is to listen to ajax responses. Then you can use the very same observable to trigger a new request, even with different parameters.

I used RxJS for a while and from there I know you can alter and combine different observables. For instance you have a list of items. You want to refresh/load that list on page load, when you click the “refresh” button and when you hit the “R” key on your keyboard. You can combine those events to one single observable and call the load function only once in your code. You can get very creative there.

MaxArt wrote

Alas no, you can’t use Proxies to reimplement Object.observe, as a Proxy object is a new object, and that one should be used to trigger its handlers. Check out here: https://github.com/MaxArt2501/object-observe/blob/master/doc/index.md#under-the-hood

That being said, you can indeed use Proxy to create something similar.

Gabriel wrote

Maybe I’m late to the party, but what does this offer that is better than listeners? Looks like the same concept just different syntax. And from an outsiders perspective, a different syntax for the sake of it. I’m probably wrong though…

Andreas Linnert wrote

They are similar indeed. But there are some differences, too. The biggest one being: Events are part of the DOM API, Observables are part of the core language. That means: Event listeners are a browser only feature. Observables will be available in node.js as well.

Alan Johnson wrote

I’m not sure what other APIs Observable instances will have, but by reifying event streams as values, you gain the ability to compose them. It’s much like the advantage that promises give you over callbacks.

This paper, for instance, really blew my mind: https://infoscience.epfl.ch/record/176887/files/DeprecatingObservers2012.pdf

Alan Johnson wrote

Does this give us much advantage over generators functions? Is the idea that this allows for fan-out of events to multiple subscribers, as well as unsubscription with tear-down?

MaxArt wrote

Finally I have the time to read this, and finally we have something concrete about observables! Hopefully methods like filter and map will be specified soon, and let’s not forget about join either.

The funny thing is that in web development we’ve had “observables” since the '90s - in fact, we have events and listeners for elements, XHR objects and so on. But they’re a quite old concept, and observables are a new and fresh take on the matter.

One question: would the methods next, complete and error be called synchronously or not?