RXJS Как узнать, когда доработка завершится?

Я относительно новичок в RXJS, поэтому прошу прощения, если это повторяется. Я попытался найти ответы, но не смог их найти, возможно потому, что не знаю, какие условия поиска использовать.

Я пытаюсь понять, как я могу узнать, когда завершается блок finalize вызова службы, потому что он обновляет переменную общего состояния.

Вот его stackblitz, хотя я также опубликую ниже фрагменты: https://stackblitz.com/edit/angular-ivy-xzvkjl

У меня есть приложение Angular со службой, которая устанавливает общий флаг isLoading в значение true, запускает HTTP-запрос, а затем использует finalize, чтобы вернуть флаг isLoading в значение false, так что независимо от успеха или ошибки элементы, которые проверяют флаг isLoading знайте, что HTTP-запрос больше не обрабатывается.

Я упростил этот сценарий до отдельных методов вместо отдельных классов:

isLoading = false;

public ngOnInit() {
  this.serviceCall().subscribe(
    next => {
      console.log("value of isLoading in next handler: " + this.isLoading);
    },
    err => {
      console.log("value of isLoading in error handler: " + this.isLoading);
    },
    () => {
      console.log("value of isLoading in complete handler: " + this.isLoading);
    }
  );
}

private serviceCall() {
  this.isLoading = true;
  return this.httpCall().pipe(
    tap(value => console.log(value)),
    finalize(() => {
      this.isLoading = false;
      console.log("Value of isLoading in serviceCall finalize: " + this.isLoading);
    })
  );
}

private httpCall() {
  return new Observable(subscriber => {
    console.log("Starting emissions");
    subscriber.next(42);
    subscriber.next(100);
    subscriber.next(200);
    console.log("Completing emissions");
    subscriber.complete();
  });
}

Я был удивлен, обнаружив, что вывод этого примера

Стартовые выбросы

42

значение isLoading в следующем обработчике: true

100

значение isLoading в следующем обработчике: true

200

значение isLoading в следующем обработчике: true

Завершение выбросов

значение isLoading в полном обработчике: true

Значение isLoading в serviceCall finalize: false

Почему finalize из serviceCall вызывается ПОСЛЕ полного обработчика блока подписки ngOnInit? И как мне узнать, когда serviceCall завершил свои манипуляции с общей переменной, если не через завершенный обработчик?


person AForsberg    schedule 20.01.2021    source источник


Ответы (1)


О finalize

Это сводится к тому, как реализован finalize. Я согласен, что, возможно, это не совсем интуитивно. Я являюсь частью расколотой фракции, которая считает, что способ его реализации сейчас является интуитивным.

Рассмотрим наблюдаемый объект, подписка на который была отменена до того, как что-либо испускает. Я ожидал, что finalize все еще сработает, но я не ожидал, что complete уведомление будет отправлено моему наблюдателю.

Шесть из одного, полдюжины из другого

Как правило, последнее, что происходит с потоком, - это отказ от подписки на него. Finalize вызывается, когда поток отменяется. Это происходит после полного или ошибочного выброса.

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


По возможности избегайте побочных эффектов.

В общем, такие побочные эффекты, как установка глобальных переменных и их последующая проверка в том же pipeline, считаются неприятным запахом кода. Если вместо этого вы немного больше опираетесь на функциональный подход, который пропагандируют потоки RxJS, подобные проблемы должны исчезнуть.


Быстро в сторону:

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

Подумайте, что происходит, когда я добавляю задержку в ваш поток:

private serviceCall() {
  this.isLoading = true;
  return this.httpCall().pipe(
    tap(value => console.log(value)),
    finalize(() => {
      this.isLoading = false;
      console.log("Value of isLoading in serviceCall finalize: " + this.isLoading);
    }),
    delay(0)
  );
}

Вы можете подумать, что задержка в 0 миллисекунд не имеет значения, но поскольку каждая задержка помещается в очередь микрозадач JS, вы заметите заметную разницу в том, как работает ваш код. Прежде чем ваша подписка будет вызвана с первым значением, isLoading уже будет ложным.

Это потому, что все до задержки выполняется синхронно и завершится до запуска очереди микрозадач. Все, что происходит после delay(0), выполняется асинхронно и завершится, когда JS в следующий раз будет готов запустить очередь микрозадач.


Решение наименьшего сопротивления

Это не идиоматический RxJS, но в этом случае он будет работать так, как вы ожидаете, что finalize будет работать.

Вы можете использовать оператор крана, чтобы поймать complete излучение. Поскольку касание сработает на complete, оно должно сработать до subscribe и, следовательно, будет работать для вашего варианта использования.

function serviceCall() {
  const setIsLoading = bool => (_ = null) => this.isLoading = bool;
  
  return defer(() => {
    setIsLoading(true)();
    return this.httpCall().pipe(
      tap({
        next: console.log,
        error: setIsLoading(false),
        complete: setIsLoading(false)
      })
    );
  });

}
person Mrk Sef    schedule 20.01.2021
comment
Спасибо, что помогли мне понять суть доработки! Однако с решением, которое вы дали, я больше не получаю того преимущества, которое я использовал от блока finalize, а именно обновления значения isLoading как для успешных, так и для ошибочных ситуаций. Если я адаптирую ваше решение так, чтобы канал также включал catchError с тем же кодом, который очищает флаг и повторно выдает ошибку, у меня будет полное решение. Это кажется таким грязным по сравнению с финализацией. Я действительно хочу решить проблему запаха кода, но пока я сосредоточен только на преобразовании AngularJS в Angular без слишком большого количества изменений логики. - person AForsberg; 20.01.2021
comment
Вы также можете увидеть ошибки в операторе касания, чего бы он ни стоил (вы не можете с ними справиться, но сделайте это в другом месте, если хотите). Я обновил свое решение, чтобы сделать это за вас. - person Mrk Sef; 20.01.2021
comment
Фактически, еще одно быстрое обновление, вы, вероятно, захотите установить this.isLoading = true, когда serviceCall() подписан, а не когда он вызывается. В зависимости от того, как будет составляться serviceCall () позже, вы можете создать свой наблюдаемый объект задолго до того, как подписаться на него (например, обычно с оператором concat). Defer просто вызывает свою фабричную функцию после подписки, так что это быстрое решение этой проблемы. - person Mrk Sef; 20.01.2021