map 操作符之间的差异

Map 操作符之间的差异

在 RxJS 中有多种高级的 map 操作符,如:concatMapmergeMapswitchMapexhaustMap,这些高阶 map 操作符与普通的 map 操作符有所不同。普通 map 操作符只是将数据映射为另一个数据,高级 map 操作符是将数据映射为 Observable 对象。

在 RxJS v4 中 mergeMap 叫 flatMap,后来觉得从名称上不好理解 mergeMap 所做的操作,从 v5 开始改名为 mergeMap。同时为了向后兼容,并未废弃 flatMap,flatMap 作为 mergeMap 的一个别名存在;因此,可以用 flatMap 替代 mergeMap。

下面通过一个简单的场景来寻找高级 Map 操作符之间的差异:

提供一些用户 ID,根据这些 ID 查询用户的 Todo List

接下来看下这些 Map 操作符具体如何来实现这个需求:

1
2
3
4
5
6
queryTodo(userID: number): Observable<any> {
const todoURL: string = "https://jsonplaceholder.typicode.com/todos";

const url: string = `${todoURL}?userId=${userID}`;
return this.http.get<Todo[]>(url);
}
  • mergeMap

    1
    2
    3
    4
    5
    6
    const user$ = of(1, 2, 3, 4).pipe(
    mergeMap(userID => this.queryTodo(userID)),
    tap(user => console.warn(JSON.stringify(user, ["userId"])))
    ).subscribe(res => {
    console.info(res);
    });

    通过截图可以看到,mergeMap 会顺序去请求用户的 Todo List,但最终返回的 Observable 的顺序却为乱序。

    mergeMap 是 mergeAll 和 map 操作符的组合。mergeMap 操作符内部产生的 Observable 对象之间是并行的,谁先有结果,则先输出到下游,也就是内部 Observable 产生数据立即传给下游

  • concatMap

    1
    2
    3
    4
    5
    6
    const user$ = of(1, 2, 3, 4).pipe(
    concatMap(userID => this.queryTodo(userID)),
    tap(user => console.warn(JSON.stringify(user, ["userId"])))
    ).subscribe(res => {
    console.info(res);
    });

    从截图可以看到,返回的 Todo List 是顺序的。

    concatMap 是 concatAll 和 map 的组合。concatMap 操作符的内部大概的流程是:产生一个内部 Observable 后,立即订阅,每产生一个数据都传给下游,当前 Observable 完结后取消订阅,然后订阅下一个 Observable。

  • switchMap

    1
    2
    3
    4
    5
    6
    const user$ = of(1, 2, 3, 4).pipe(
    switchMap(userID => this.queryTodo(userID)),
    tap(user => console.warn(JSON.stringify(user, ["userId"])))
    ).subscribe(res => {
    console.info(res);
    });

    可以看到 switchMap 把前面用户的请求都取消了,只保留了最后一个用户的请求。

    switchMap 是 switch 和 map 的组合。对于 meigeMap 和 concatMap 而言,内部产生的 Observable 优先级都一样,但在 switchMap 中后产生的 Observable 对象优先级总是比前面的高,也就是最新的 Observable 优先级最高;只有要新的 Observable 产生,那么会立即取消订阅之前的 Observable,然后订阅最新的 Observable。一句话概括就是,只要有新的 Observable 产生,switchMap 会切换到最新的内部 Observable 对象。

  • exhaustMap

    1
    2
    3
    4
    5
    6
    const user$ = of(1, 2, 3, 4).pipe(
    exhaustMap(userID => this.queryTodo(userID)),
    tap(user => console.warn(JSON.stringify(user, ["userId"])))
    ).subscribe(res => {
    console.info(res);
    });

    exhaustMap 操作符只去请求了第一个用户的 Todo List,忽略了其余用户的请求。

    exhaustMap 是 exhaust 和 map 的组合。与 switchMap 操作符相反的是,对于 exhaust 操作符而言先产生的 Observable 优先级更高。在前面产生的 Observable 未完结之前,exhaustMap 不会去订阅新产生的 Observable。

    将上面代码替换下

    1
    2
    3
    4
    5
    6
    7
    8
    const user$ = interval(100).pipe(
    take(4),
    map(userID => userID + 1),
    exhaustMap(userID => this.queryTodo(userID)),
    tap(user => console.warn(JSON.stringify(user, ["userId"])))
    ).subscribe(res => {
    console.info(res);
    });

    可以看到第二个用户的请求被忽略了,因为第一个请求耗时 127ms,第二个 Observable 对象是在第一个 Observable 对象完结前产生的。