Тема указана в неправильном порядке при вызове следующего оператора

У меня есть предмет, затем я применяю к нему оператор map(x => x/*not the real function*/). Но оператор карты имеет некоторые побочные эффекты, в некоторых случаях он выдает новое значение.

Вот пример:

const sub = new Subject();
const emits = [];
const mapped = [];
const emit$ = sub.asObservable().subscribe(x => emits.push(x));
const data$ = sub.asObservable().pipe(
    map(x => {
        return x;
    }),
    tap(x => mapped.push(x)),
    tap(x => {
        if (x % 2 === 0) {
            sub.next(2333);
        }
        if (x === 2333) {
            sub.next(1111);
        }
    })
);
const datas = [];
data$.subscribe(x => {
    datas.push(x);
});
sub.next(1);
sub.next(2);
setTimeout(() => {
    console.log('emits: ',emits);
    console.log('mapped: ', mapped);
    console.log('datas: ', datas);
}, 10);

когда входная последовательность равна [1, 2], подписчик субъекта получит [1, 2, 2333, 1111], но подписчик сопоставленного наблюдаемого получит [1, 1111, 2333, 2].

ОБНОВИТЬ:

Я перемещаю побочные эффекты в оператор tap и сохраняю сопоставленные выбросы в массив, после чего результат становится таким:

emits: [1, 2, 2333, 1111]
mapped: [1, 2, 2333, 1111]
datas: [1, 1111, 2333, 2]

Вот вопросы:

  1. Это правильное поведение?
  2. Должен ли я выделять новое значение в операторах?

person Tao Zhu    schedule 06.05.2018    source источник


Ответы (1)


Поведение здесь не интуитивно понятно, потому что Subject имеют семантику, отличную от обычных Observables.

Согласно документации, Subject имеют семантику, аналогичную EventEmitters: они отслеживают их подписчики внутри и синхронно вызывают все слушатели, когда генерируется новое событие.

В вашем примере оператор tap синхронно передает новые события в data$ Observable. Таким образом, каждый раз, когда он вызывает next(), он немедленно запускает цепочку pipe для нового значения до того, как обработчик tap() вернется.

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

Когда вы запустите пример, вы увидите, что цепочка pipe выполняется для 1111 и 2333 до возврата последнего обработчика tap для 2. Вы также увидите, что стек вызовов событий 1111 и 2333 по-прежнему содержит вызовы pipe/tap для события 2.

Почему наблюдаемая emit$ имеет события в другом порядке? emit$ подписан непосредственно на Subject, поэтому его прослушиватель запускается синхронно, как только вызывается next. С точки зрения emit$ порядок вызовов next равен [1, 2, 2333, 1111].

datas$, с другой стороны, должен ждать, пока последний оператор tap() не вернется, прежде чем абонент будет вызван. Таким образом, когда событие 2 проходит через финальное tap, datas$ не видит его до тех пор, пока не завершится обработка 1111 и 2333.

Следует ли выдавать новые значения в операторах?

Как правило, нет. Почему? Потому что очень сложно понять, что происходит, даже в таком относительно простом примере, как этот (где все вызывается синхронно).

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

Как правило, вводите новые значения в операторы только в том случае, если вы уверены, что вам нужна точная семантика. Если есть способ представить вашу логику, не делая этого (например, используя обычные не-Subject Observables) или, что еще лучше, вообще не используя Subject, тогда будет намного проще рассуждать о коде.

person bgran    schedule 06.05.2018
comment
Что, если я использую setTimeout(() => sub.next(value), 0) для выдачи значений в операторах, я обновил свой примерный код. Можно ли решить проблему таким образом? - person Tao Zhu; 06.05.2018
comment
На самом деле здесь нет проблем: просто так работает Subject. Если вы запланируете вызовы next с setTimeout, возникнет состояние гонки: мы не знаем, будут ли обработчики setTimeout, запланированные в tap, запускаться до setTimeout, который регистрирует массивы. Использование setTimeout приведет к тому, что emits, mapped и data будут иметь одинаковый порядок. - person bgran; 06.05.2018