I read on docs that:
If you pass to concat the same Observable many times, its stream of values will be "replayed" on every subscription, which means you can repeat given Observable as many times as you like. If passing the same Observable to concat 1000 times becomes tedious, you can always use repeat.
https://rxjs.dev/api/index/function/concat
And my question is: why does it work that way? I would expect it to return only values from first observable and complete after that (since all the others would be completed also because we are refering to the same observable). It would be coherent with other concat()
behaviour to work that way.
CodePudding user response:
The strategy of the concat
is to make subscription to the first provided observable, after it finish, concat makes subscription to the second observable and so on.
Second aspect of that is that you provide cold observables. It means every time the subscription is done, observable starts from the beginning. More about cold and hot observables you could find here What are the Hot and Cold observables?
Here is test playground with marble diagrams for better understanding.
const {concat} = rxjs;
const {take, mergeMap, tap} = rxjs.operators;
const {TestScheduler} = rxjs.testing;
const {expect} = chai;
const test = (testName, testFn) => {
try {
testFn();
console.log(`Test PASS "${testName}"`);
} catch (error) {
console.error(`Test FAIL "${testName}"`, error.message);
}
}
const createTestScheduler = () => new TestScheduler((actual, expected) => {
expect(actual).deep.equal(expected);
});
test('should concat two cold observables', () => {
const testScheduler = createTestScheduler();
testScheduler.run((helpers) => {
const { cold, hot, expectObservable } = helpers;
const init$ = cold(' --e-------------------|');
const letters$ = cold(' --a--b--c| ');
const numbers$ = cold(' --0--1--2| ');
const resultMarble = ' ----a--b--c--0--1--2--|';
const result$ = init$.pipe(
mergeMap(() => concat(letters$, numbers$))
);
expectObservable(result$).toBe(resultMarble);
});
});
test('should conconcat two hot observables', () => {
const testScheduler = createTestScheduler();
testScheduler.run((helpers) => {
const { cold, hot, expectObservable } = helpers;
// the ^ character marks the zero time of hot observable
const init$ = cold(' ----e---------------|');
const letters$ = hot('-----^--a--b--c--d--------');
const numbers$ = hot('---0-^----1--2--3--4------');
const resultMarble = ' ------b--c-3--4-----|';
const result$ = init$.pipe(
mergeMap(() => concat(
letters$.pipe(take(2)),
numbers$.pipe(take(2))
))
);
expectObservable(result$).toBe(resultMarble);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.1.2/chai.js"></script>
<script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
CodePudding user response:
Consider the following:
// Defining a function
const addTwoNumbers = a => b => () => {
console.log(`Adding ${a} ${b}`);
return a b;
}
// Defining another function
const increment = addTwoNumbers(1);
// Defining another function
const incFive = increment(5);
// Invoking the final function to get a result
const result = incFive();
// Result is 6
console.log(`Result is ${result}`);
// Invoke 3 more times
[incFive, incFive, incFive].forEach(
fn => fn()
);
Notice how we're running the code 1 5
every time we invoke incFive
? If you run this code you'll see
Adding 1 5
Result is 6
Adding 1 5
Adding 1 5
Adding 1 5
The code in the function is "run" 4 time in total even though the function is defined only once.
That's sort of how observables work! subscribe
says to run the observable, so if you subscribe 10 times, you run the observable 10 times.
Of course, this changes a bit if you're dealing with a "hot" observable. Using concat
to repeat an observable that listens to button licks (for example) doesn't make much sense. Using concat
to request updates from a server, however, makes a lot more sense.
It's nice to define an observable once and be able to re-use it. Above, the line below "invoke 3 more times" could be rewritten as:
// Redefine 3 more times
[addTwoNumbers(1)(5),addTwoNumbers(1)(5), addTwoNumbers(1)(5)].forEach(
fn => fn()
);
But that's a hassle! Also, sometimes you want to define a function one place and invoke it in a separate place altogether. By then you may not know how to redefine it. The same is true with observables.