JavaScript 📜
April 10

Defer Observable в RxJs

Представьте, что нам нужно создать оператор, который принимает функцию и выполняет ее только при первом событии. Реализуем его, назовав его tapOnce:

function tapOnce<T>(fn: Function): OperatorFunction<T, T> {
  return function(source: Observable<any>) {
    let run = false;
    return source.pipe(
      tap(() => {
        if (!run) {
          fn();
          run = true;
        }
      })
    );
  };
}

Код простой: используется оператор tap для запуска переданной функции один раз на основе значения переменной run. Теперь воспользуемся созданным оператором:

const source = interval(5000).pipe(
  tapOnce(() => console.log('🦊')
));

source.subscribe(console.log);

Все работает хорошо. Мы видим эмодзи один раз, во время первого события. Теперь добавим еще одного подписчика:

const source = interval(5000).pipe(
  tapOnce(() => console.log('🦊')
));

source.subscribe(console.log);
source.subscribe(console.log);

Если мы посмотрим в консоль, мы увидим только один эмодзи. Проблема заключается в том, что оба подписчика используют одну и ту же лексическую область видимости и, следовательно, ссылается на ту же переменную run. Нам нужен способ отложить создание Observable до момента, когда кто-то подпишется на него. Defer приходит на помощь:

import { defer } from 'rxjs';

function tapOnce<T>(fn: Function): OperatorFunction<T, T> {
  return function(source: Observable<any>) {
    return defer(() => {
      let run = false;
      return source.pipe(
        tap(() => {
          if (!run) {
            fn();
            run = true;
          }
        })
      );
    });
  };
}

Observable defer принимает функцию, которая возвращает ObservableInput. Код внутри defer выполняется только при подписке, а не во время создания. Используя этот подход и благодаря замыканиям JS, каждый подписчик получает свою собственную лексическую область видимости.

Чтобы понять как работает этот оператор, создадим свою упрощенную реализацию:

function defer(observableFactory: () => ObservableInput<any>) {
  return new Observable(subscriber => {
    const source = observableFactory();
    return source.subscribe(subscriber);
  });
}

defer возвращает новый Observable, который при подписке вызывает функцию-фабрику и использует ее в качестве источника.

Рассмотрим еще несколько случаев использования, где мы можем воспользоваться Observable defer. Предположим, у нас есть выражение, которое мы хотим вычислить только при подписке. Например:

const randNum = of(Math.random());
 
randNum.subscribe(console.log);
randNum.subscribe(console.log);

В этом примере каждый подписчик получит одно и то же случайное значение. Мы можем решить эту проблему, используя defer, чтобы выражение оценивалось только тогда, когда кто-то подписался на Observable, и не заранее:

const randNum = defer(() => of(Math.random()));

randNum2.subscribe(console.log);
randNum2.subscribe(console.log);

// The same concept as using a function
const randNum = () => of(Math.random());
randNum2().subscribe(console.log);
randNum2().subscribe(console.log);

Еще один случай использования - когда мы хотим отложить выполнение промиса до момента подписки:

// This already executing regardless the numbers of handlers
const promise = new Promise(resolve => {
  resolve();
});

// Deferring the creation of the promise until someone subscribes
const promiseDefered = defer(() => {
  return new Promise(resolve => {
    resolve();
  });
});

promiseDefered.subscribe(console.log);

Промисы по своей природе выполняются всегда, даже если нет обработчиков результата. Мы можем использовать отложенное наблюдаемое, чтобы сделать выполнение обещания более похожим на rxjs потоки, то есть выполнение произойдет только при подписке.

Это вольный перевод материала