RxJS系列04:操作符 Operators(上)

868 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

RxJS(Reactive Extensions for JavaScript) 是一个非常强大的 JS 库,我们可以使用它轻松编写异步代码。

在本系列文章中,我将带领你学习 RxJS 的最新版本,我们会重点关注如何使用响应式编程范式来解决你在日常工作中碰到的问题。所以这是一个偏实战的系列文章。

在本系列文章中,你将学会 RxJS 中的核心组件是如何使用和运作的。

通过学习这个系列文章,你将亲自使用 RxJS 完成一个完整的项目开发,在这个项目中,你将了解如何处理 DOM 事件、如何构建响应式本地数据库等内容。

操作符的概念

在前面的文章中,我们了解了响应式编程范式的理念,学会了使用 Observable 来简化编程,还了解了管道的概念,数据转换会发生在这里。我们知道在数据转换的过程中会用到很多操作符,在这篇文章中我们继续深入学习操作符这个核心概念。

什么是操作符?

操作符是一个纯函数,可以在任何时候处理管道中流动的数据。

纯函数是指不会改变任何外部状态的函数,也不会改变朝管道推送数据的 observable,而是会生成一个新的 observable,这个新的 obervable 会顺着流继续向下推送。

操作符的类型

RxJS 中存在大量的操作符,我们可以根据它们的功能将它们分为不同的类型:

  1. 创建操作符
  2. 组合操作符
  3. 过滤操作符
  4. 转换操作符

我们需要熟悉每种类型下的操作符,灵活地组合它们,构建出功能强大的数据管道,才能在实际开发中解决掉各种问题。

创建操作符

在之前的文章中,我们已经用到了一些 RxJS 的操作符,它们可以用于包装不同的数据源来创建一个新的 Observable。例如 from 和 of。

下面是一些常用的创建操作符。

from

from 操作符可以将数组、promise 或者 iterable 等数据包装到一个 Observable 中。

下面的代码演示了将一个字符串数组包装到 Observable 中,每个新的订阅者都可以使用数据源中的所有数据。

import { from } from 'rxjs';

const DATA_SOURCE = ['希望', '上海', '早日', '解封'];
const observable$ = from(DATA_SOURCE)

observable$.subscribe(console.log)

of

of 操作符可以从数据序列中创建 Observable。of 和 from 之间的区别在于,of 从一个可以是数组的可迭代对象中创建 Observable。

通过下面这个示例和第一个示例进行对比会更容易理解。

import { of } from 'rxjs';

const DATA_SOURCE = ['希望', '上海', '早日', '解封'];
const observableArray$ = of(DATA_SOURCE)

console.log("数组数据源")
observableArray$.subscribe(console.log)

console.log("\n")
console.log("序列数据源")
const observableSequence$ = of('希望', '上海', '早日', '解封')

observableSequence$.subscribe(console.log)

range

range 是一个非常有用的操作符,它可以发出从给定的数字范围内的所有数字。

在迭代数组时非常有用。

import { range } from 'rxjs'

const data = ['item 1', 'item 2', 'item 3']
const observableRange$ = range(0, data.length)

observableRange$.subscribe(index => console.log(data[index]))

interval

interval 可以定期发送某个值。在处理交互式的定时通知时非常有用。

import { interval } from 'rxjs'

const fastObservable$ = interval(100)

const subscription = fastObservable$.subscribe(() => console.log('接收到值'))

setTimeout(() => {
    subscription.unsubscribe();
    console.log('1s 过后');
}, 1000)

timer

timer 的作用是在某个时刻后才开始发送值。拿 interval 中的例子继续举例,我们希望先等待 1 秒后再每隔 100 毫秒发送值。timer 操作符就可以轻松完成这个任务。

下面是示例。

import { timer } from 'rxjs'

const fastObservable$ = timer(1000, 100)

const subscription = fastObservable$.subscribe(() => console.log('接收到值'))
console.log('1s 过后')
setTimeout(() => {
    subscription.unsubscribe();
    console.log('2s 过后');
}, 2000)

有时我们希望经过一段时间后开始定时生成值,在这种情况下,timer 操作符会非常有用。

defer

defer 是一个将函数作为参数的操作符,这个函数只有在订阅者发起订阅时才会执行。对于时间相关的操作它会非常有用。比如我们需要获取当前执行时间。

import { defer } from 'rxjs'

const functionToBeExecutedOnSubscribe = () => console.log('现在开始执行,执行时间:', Date.now())
const observable1$ = defer(functionToBeExecutedOnSubscribe)
observable1$.subscribe()

为了保持纯函数,我们传递给 defer 操作符的函数必须返回一个 Observable。

import { defer, of } from 'rxjs'

const functionToBeExecutedOnSubscribeReturnsObservable = () => of('val1', 'val2')
const observable2$ = defer(functionToBeExecutedOnSubscribeReturnsObservable)
observable2$.subscribe(console.log)

fromEvent

当我们在开发复杂的 SPA 应用时,最有用的操作符是 fromEvent,我们可以使用这个操作符将鼠标和键盘事件包装到 Observable 中。

const keysDown$ = fromEvent(document, 'keydown')
keysDown$.subscribe(console.log)

这些创建操作符可以满足我们的大部分需求,

过滤操作符

我们知道,操作符是可以组合使用的,其中最重要的就是过滤操作符。

RxJS 中的数据管道必须在 pipe 函数内定义:

Observable.pipi(operator1, operator2, operator3)

filter

filter 操作符需要一个函数,通过流传递的值会到达这个函数,只有这个函数返回 true 时值才允许继续流动。

下面是只允许偶数到达订阅者的示例。

import { from } from 'rxjs'
import { filter } from 'rxjs/operators'

const observable$ = from([1, 2, 3, 4, 5, 6])

observable$.pipe(
    filter(val => val % 2 == 0)
).subscribe(console.log)

first

first 操作符非常简单,它只允许第一个发出的值通过。

但是 first 有一个特点,如果 Observable 在发出任何值之前就完成了,那么它就会抛出错误,因为这不是在预期范围以内的行为。另外一个操作符 take 可以忽略这种情况。

import { from } from 'rxjs'
import { first } from 'rxjs/operators'

const observable$ = from([1, 2, 3, 4, 5, 6])

observable$.pipe(
    first()
).subscribe(console.log)

我们还可以将一个函数和一个默认值作为 first 的参数,这个函数返回一个布尔值,true 表示通过。第一个通过函数的值将会被送达订阅者。

import { from } from 'rxjs'
import { first } from 'rxjs/operators'

const observable$ = from([1, 2, 3, 4, 5, 6])

observable$.pipe(
    first(val => val > 6, -1)
).subscribe(console.log)

last

last 的用法和 first 几乎一致,但它返回的是 Observable 发送的最后一个值。last 同样可以接收一个过滤函数和一个默认值作为参数。

import { from } from 'rxjs'
import { last } from 'rxjs/operators'

const observable$ = from([1, 2, 3, 4, 5, 6])

observable$.pipe(
    last()
).subscribe(console.log)

observable$.pipe(
    last(val => val > 2, -1)
).subscribe(console.log)

take

如果我们只需要前 N 个值,对后续的值不感兴趣。可以使用 take 操作符。

import { from } from 'rxjs'
import { take } from 'rxjs/operators'

const observable$ = from([1, 2, 3, 4, 5, 6])

observable$.pipe(
    take(3)
).subscribe(console.log)

skip

skip 和 take 恰恰相反,如果我们对前 N 个值不感兴趣,可以使用 skip 跳过这些值。

import { from } from 'rxjs'
import { skip } from 'rxjs/operators'

const observable$ = from([1, 2, 3, 4, 5, 6])

observable$.pipe(
    skip(2)
).subscribe(console.log)

takeUntil

如果我们想获取/跳过前 N 个值,但这个 N 不确定,而是直到某个条件满足时,才停止获取,该怎么做呢?

假设我们订阅了一个每 100 毫秒发出一个值的 Observable,直到用户跳转到另一个页面后,才取消订阅。takeUntil 可以实现这个功能。

下面是代码示例:

import { Subject, interval } from 'rxjs'
import { takeUntil } from 'rxjs/operators'

const pageNavigationSubject$ = new Subject()

const observable$ = interval(100)

observable$.pipe(
    takeUntil(pageNavigationSubject$)
).subscribe(() =>console.log('接收到值'))

setTimeout(() => {
    pageNavigationSubject$.next('跳转到下一个页面')
    console.log('用户已跳转到另一个页面')
}, 1000)

skipUntil

再来看另一种情况,在 HTTP 请求得到响应之前,某个订阅者需要跳过所有的值。

这种情况可以使用 skipUntil 来解决。下面是示例:

import { Subject, interval } from 'rxjs'
import { skipUntil } from 'rxjs/operators'

const httpSubject$ = new Subject()

const observable$ = interval(100)

intervalObservableSub$ = observable$.pipe(
    skipUntil(httpSubject$)
).subscribe(() =>console.log('接受到值'))

setTimeout(() => {
    httpSubject$.next('HTTP 请求得到响应')
    console.log('HTTP 请求已结束')
}, 1000)

setTimeout(() => {
    intervalObservableSub$.unsubscribe()
}, 2000)

takeWhile 和 skipWhile

takeUntil 和 skipUntil 可以在另一个 Observable 发送值时停止/启动接收值。但是当我们处理更简单更轻量的逻辑时,takeWhile 和 skipWhile 就派上用场了。

它们的逻辑和 until 的逻辑很像,但是 while 可以接收一个函数作为参数,该函数返回一个布尔值,当函数返回 true 时,则停止/启动接收值。

import { from } from 'rxjs'
import { takeWhile, skipWhile } from 'rxjs/operators'

const observable$ = from([0, 0, 0, 1, 0])

console.log('Take While')
observable$.pipe(
    takeWhile(x => x < 1)
).subscribe(console.log)

console.log('Skip While')
observable$.pipe(
    skipWhile(x => x < 1)
).subscribe(console.log)

distinctUntilChanged

distinctUntilChanged 运算符的作用很单一,就是过滤掉重复的值。

import { from } from 'rxjs'
import { distinctUntilChanged } from 'rxjs/operators'

const observable$ = from([1, 1, 1, 2, 3, 4, 5, 6])

observable$.pipe(
    distinctUntilChanged()
).subscribe(console.log)

debounceTime

若果我们的网站中将要开发一个实时搜索功能,用户在键盘上输入关键字后,展示搜索结果。我们最先想到的思路是通过监听 keydown 事件来触发一个 HTTP 请求。看上去是没什么问题的。

但是我们要考虑一种情况,有些人的打字速度非常快,可能一秒钟内会连续输入十几个字符,从而导致产生很多个没用的 HTTP 请求。没用是因为用户只会对他们停止输入的那个时间的搜索结果感兴趣,而不是输入过程中的结果。

在任何这种实时搜索的场景中,debounce(防抖) 都可以帮助我们解决这个问题。

我们现在将实现思路调整为,当用户停止输入 50 毫秒后才发出一个 HTTP 请求。

RxJS 提供了 debounceTime 操作符,它仅在上次接收到上次发送的值后又经过一段时间后才又发送的值。示例代码如下:

import { Observable } from 'rxjs'
import { debounceTime } from 'rxjs/operators'

const observable$ = new Observable(observer => {
    setTimeout(() => observer.next('R'), 20)
    setTimeout(() => observer.next('x'), 30)
    setTimeout(() => observer.next('J'), 35)
    setTimeout(() => observer.next('S'), 50)

    setTimeout(() => observer.next('系'), 120)
    setTimeout(() => observer.next('列'), 160)

    setTimeout(() => observer.next('0'), 180)
    setTimeout(() => observer.next('4'), 200)
})

observable$.pipe(debounceTime(50)).subscribe(val => console.log('发送 HTTP 请求! 当前值是: ' + val))

它的工作流程图如下:

image.png

debounceTime 的另一个常见场景是编辑器的自动保存功能。

记住,任何有可能连续高频输入的场景都可能会需要它。

throttleTime

我们再来考虑实时搜索的另一种情况,我们的需求变成了每等待一段时间,就要发送一次 HTTP 请求,而不是等待用户的暂停输入。

这时可以使用 throttleTime 来实现,它可以立即发出一次 HTTP 请求,接着每隔一段时间定期继续发送 HTTP 请求。

它的工作流程如图所示:

image.png

下面是对应的代码示例:

import { Observable } from 'rxjs'
import { throttleTime } from 'rxjs/operators'

const observable$ = new Observable(observer => {
    setTimeout(() => observer.next('R'), 20)
    setTimeout(() => observer.next('x'), 30)
    setTimeout(() => observer.next('J'), 35)
    setTimeout(() => observer.next('S'), 50)

    setTimeout(() => observer.next('系'), 120)
    setTimeout(() => observer.next('列'), 160)

    setTimeout(() => observer.next('0'), 180)
    setTimeout(() => observer.next('4'), 200)
})

observable$.pipe(throttleTime(20)).subscribe(val => console.log('发送 HTTP 请求! 当前值是: ' + val))