Skip to content

Instantly share code, notes, and snippets.

@vasilionjea
Last active April 18, 2024 04:22
Show Gist options
  • Save vasilionjea/c44222a1442c9a1dfddfa3d092ac9e51 to your computer and use it in GitHub Desktop.
Save vasilionjea/c44222a1442c9a1dfddfa3d092ac9e51 to your computer and use it in GitHub Desktop.

JavaScript Observables

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;
    });
  }
}

Interval observable example

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();
    }
  });

DOM event observable example

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} `;
    }
  });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment