RxJS

141 阅读13分钟

RxJS 是一个库,它通过使用 observable 序列来编写异步和基于事件的程序。它提供了一个核心类型 Observable,附属类型 (Observer、 Schedulers、 Subjects) 和受 [Array#extras] 启发的操作符 (map、filter、reduce、every, 等等),这些数组操作符可以把异步事件作为集合来处理。

ReactiveX 结合了 **观察者模式 ** 迭代器模式使用集合的函数式编程,以满足以一种理想方式来管理事件序列所需要的一切。

在 RxJS 中用来解决异步事件管理的的基本概念是:

  • Observable (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。
  • Observer (观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。
  • Subscription (订阅): 表示 Observable 的执行,主要用于取消 Observable 的执行。
  • Operators (操作符): 采用函数式编程风格的纯函数 (pure function),使用像 mapfilterconcatflatMap 等这样的操作符来处理集合。
  • Subject (主体): 相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。
  • Schedulers (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeoutrequestAnimationFrame 或其他。

Observable (可观察对象)

创建 Observables

Observables 是使用 Rx.Observable.create 或创建操作符创建的,并使用观察者来订阅它,然后执行它并发送 next / error / complete 通知给观察者,而且执行可能会被清理。像 Observer (观察者) 和 Subscription (订阅)。

下面的示例创建了一个 Observable,它每隔一秒会向观察者发送字符串 'hi'

var observable = Rx.Observable.create(function subscribe(observer) {
  var id = setInterval(() => {
    observer.next('hi')
  }, 1000);
});

servables 可以使用 create 来创建, 但通常我们使用所谓的创建操作符, 像 offrominterval、等等。

订阅 Observables

示例中的 Observable 对象 observable 可以订阅,像这样:

observable.subscribe(x => console.log(x));

订阅 Observable 像是调用函数, 并提供接收数据的回调函数。

执行 Observables

Observable 执行可以传递三种类型的值:

  • "Next" 通知: 发送一个值,比如数字、字符串、对象,等等。

  • "Error" 通知: 发送一个 JavaScript 错误 或 异常。

  • "Complete" 通知: 不再发送任何值。

在 Observable 执行中, 可能会发送零个到无穷多个 "Next" 通知。如果发送的是 "Error" 或 "Complete" 通知的话,那么之后不会再发送任何通知了。

清理 Observables

var observable = Rx.Observable.from([10, 20, 30]);
var subscription = observable.subscribe(x => console.log(x));
// 稍后:
subscription.unsubscribe();

当你订阅了 Observable,你会得到一个 Subscription ,它表示进行中的执行。只要调用 unsubscribe() 方法就可以取消执行。

一些人声称 Observables 是异步的。那不是真的。如果你用日志包围一个函数调用,像这样:

var foo = Rx.Observable.create(function (observer) {
  console.log('Hello');
  observer.next(42);
});
console.log('before');
foo.subscribe(function (x) {
  console.log(x);
});
console.log('after');

output

"before"
"Hello"
42
"after"

这证明了 foo 的订阅完全是同步的,就像函数一样。

Observables 传递值可以是同步的,也可以是异步的。

// 当订阅下面代码中的 Observable 的时候会立即(同步地)推送值`1`、`2`、`3`,然后1秒后会推送值`4`,再然后是完成流:
var observable = Rx.Observable.create(function (observer) {
  // 同步
  observer.next(1);
  observer.next(2);// “返回”另外一个值
  observer.next(3);// “返回”另外一个值
  // 异步执行
  setTimeout(() => {
    observer.next(4);
    observer.complete();
  }, 1000);
});
// 要调用 Observable 并看到这些值,我们需要订阅 Observable:
console.log('just before subscribe');
observable.subscribe({
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
});
console.log('just after subscribe');

控制台执行的结果:

just before subscribe
got value 1
got value 2
got value 3
just after subscribe
got value 4
done

Observer (观察者)

观察者是由 Observable 发送的值的消费者。观察者只是一组回调函数的集合,每个回调函数对应一种 Observable 发送的通知类型:nexterrorcomplete 。下面的示例是一个典型的观察者对象:

var observer = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
  complete: () => console.log('Observer got a complete notification'),
};

要使用观察者,需要把它提供给 Observable 的 subscribe 方法:

observable.subscribe(observer);

观察者只是有三个回调函数的对象,每个回调函数对应一种 Observable 发送的通知类型。

RxJS 中的观察者也可能是部分的。如果你没有提供某个回调函数,Observable 的执行也会正常运行,只是某些通知类型会被忽略,因为观察者中没有相对应的回调函数。

当订阅 Observable 时,你可能只提供了一个回调函数作为参数,它会创建一个观察者对象并使用第一个回调函数参数作为 next 的处理方法。

Subscription (订阅)

Subscription 是表示可清理资源的对象,通常是 Observable 的执行。Subscription 有一个重要的方法,即 unsubscribe,它不需要任何参数,只是用来清理由 Subscription 占用的资源。

var observable = Rx.Observable.interval(1000);
var subscription = observable.subscribe(x => console.log(x));
// 稍后:
// 这会取消正在进行中的 Observable 执行
// Observable 执行是通过使用观察者调用 subscribe 方法启动的
subscription.unsubscribe();

Subscription 基本上只有一个 unsubscribe() 函数,这个函数用来释放资源或去取消 Observable 执行

Subject (主体)

RxJS Subject 是一种特殊类型的 Observable,它允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。

每个 Subject 都是 Observable 。 对于 Subject,你可以提供一个观察者并使用 subscribe 方法,就可以开始正常接收值。

每个 Subject 都是观察者。 Subject 是一个有如下方法的对象: next(v)error(e)complete()

在下面的示例中,我们为 Subject 添加了两个观察者,然后给 Subject 提供一些值:

var subject = new Rx.Subject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(1);
subject.next(2);

下面是控制台的输出:

observerA: 1
observerB: 1
observerA: 2
observerB: 2

因为 Subject 是观察者,这也就在意味着你可以把 Subject 作为参数传给任何 Observable 的 subscribe 方法,如下面的示例所展示的:

var subject = new Rx.Subject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

var observable = Rx.Observable.from([1, 2, 3]);

observable.subscribe(subject); // 你可以提供一个 Subject 进行订阅

多播的 Observables

多播 Observable 在底层是通过使用 Subject 使得多个观察者可以看见同一个 Observable 执行。

在底层,这就是 multicast 操作符的工作原理:观察者订阅一个基础的 Subject,然后 Subject 订阅源 Observable 。下面的示例与前面使用 observable.subscribe(subject) 的示例类似:

var source = Rx.Observable.from([1, 2, 3]);
var subject = new Rx.Subject();
var multicasted = source.multicast(subject);

// 在底层使用了 `subject.subscribe({...})`:
multicasted.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
multicasted.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

// 在底层使用了 `source.subscribe(subject)`:
multicasted.connect();

multicast 操作符返回一个 Observable,它看起来和普通的 Observable 没什么区别,但当订阅时就像是 Subject 。multicast 返回的是 ConnectableObservable,它只是一个有 connect() 方法的 Observable 。

connect() 方法十分重要,它决定了何时启动共享的 Observable 执行。因为 connect() 方法在底层执行了 source.subscribe(subject),所以它返回的是 Subscription,你可以取消订阅以取消共享的 Observable 执行。

refCount 的作用是,当有第一个订阅者时,多播 Observable 会自动地启动执行,而当最后一个订阅者离开时,多播 Observable 会自动地停止执行。

示例如下:

var source = Rx.Observable.interval(500);
var subject = new Rx.Subject();
var refCounted = source.multicast(subject).refCount();
var subscription1, subscription2, subscriptionConnect;

// 这里其实调用了 `connect()`,
// 因为 `refCounted` 有了第一个订阅者
console.log('observerA subscribed');
subscription1 = refCounted.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

setTimeout(() => {
  console.log('observerB subscribed');
  subscription2 = refCounted.subscribe({
    next: (v) => console.log('observerB: ' + v)
  });
}, 600);

setTimeout(() => {
  console.log('observerA unsubscribed');
  subscription1.unsubscribe();
}, 1200);

// 这里共享的 Observable 执行会停止,
// 因为此后 `refCounted` 将不再有订阅者
setTimeout(() => {
  console.log('observerB unsubscribed');
  subscription2.unsubscribe();
}, 2000);

执行结果:

observerA subscribed
observerA: 0
observerB subscribed
observerA: 1
observerB: 1
observerA unsubscribed
observerB: 2
observerB unsubscribed

refCount() 只存在于 ConnectableObservable,它返回的是 Observable,而不是另一个 ConnectableObservable 。

BehaviorSubject

Subject 的其中一个变体就是 BehaviorSubject,它有一个“当前值”的概念。它保存了发送给消费者的最新值。并且当有新的观察者订阅时,会立即从 BehaviorSubject 那接收到“当前值”。

BehaviorSubjects 适合用来表示“随时间推移的值”。举例来说,生日的流是一个 Subject,但年龄的流应该是一个 BehaviorSubject 。

在下面的示例中,BehaviorSubject 使用值0进行初始化,当第一个观察者订阅时会得到0。第二个观察者订阅时会得到值2,尽管它是在值2发送之后订阅的。

var subject = new Rx.BehaviorSubject(0); // 0是初始值

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(3);

输出:

observerA: 0
observerA: 1
observerA: 2
observerB: 2
observerA: 3
observerB: 3

ReplaySubject

ReplaySubject 类似于 BehaviorSubject,它可以发送旧值给新的订阅者,但它还可以记录 Observable 执行的一部分。

ReplaySubject 记录 Observable 执行中的多个值并将其回放给新的订阅者。

当创建 ReplaySubject 时,你可以指定回放多少个值:

var subject = new Rx.ReplaySubject(3); // 为新的订阅者缓冲3个值

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(5);

With output:

observerA: 1
observerA: 2
observerA: 3
observerA: 4
observerB: 2
observerB: 3
observerB: 4
observerA: 5
observerB: 5

除了缓冲数量,你还可以指定 window time (以毫秒为单位)来确定多久之前的值可以记录。在下面的示例中,我们使用了较大的缓存数量100,但 window time 参数只设置了500毫秒。

var subject = new Rx.ReplaySubject(100, 500 /* windowTime */);

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

var i = 1;
setInterval(() => subject.next(i++), 200);

setTimeout(() => {
  subject.subscribe({
    next: (v) => console.log('observerB: ' + v)
  });
}, 1000);

从下面的输出可以看出,第二个观察者得到的值是345,这三个值是订阅发生前的500毫秒内发生的:

observerA: 1
observerA: 2
observerA: 3
observerA: 4
observerA: 5
observerB: 3
observerB: 4
observerB: 5
observerA: 6
observerB: 6
...

AsyncSubject

AsyncSubject 是另一个 Subject 变体,只有当 Observable 执行完成时(执行 complete()),它才会将执行的最后一个值发送给观察者。

var subject = new Rx.AsyncSubject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(5);
subject.complete();

输出:

observerA: 5
observerB: 5

AsyncSubject 和 last() 操作符类似,因为它也是等待 complete 通知,以发送一个单个值。

Operators (操作符)

尽管 RxJS 的根基是 Observable,但最有用的还是它的操作符。操作符是允许复杂的异步代码以声明式的方式进行轻松组合的基础代码单元。

以下罗列了一些常用的,更多操作符请参考RxJS官网

创建操作符

create

创建一个新的 Observable ,当观察者( Observer )订阅该 Observable 时,它会执行指定的函数。

const observable = Observable.create((observe) => {
    observe.next('value');
    observer.complete();
})
observable.subscribe(...)

of

创建一个 Observable,它会依次发出由你提供的参数,最后发出完成通知。

var source = of('Jerry', 'Anna');

observable.subscribe(...)

from

从一个数组、类数组对象、Promise、迭代器对象或者类 Observable 对象创建一个 Observable.

几乎可以把任何东西都能转化为Observable.

将一个无限的迭代器(来自于 generator)转化为 Observable。

function* generateDoubles(seed) {
  var i = seed;
  while (true) {
    yield i;
    i = 2 * i; // double it
  }
}

var iterator = generateDoubles(3);
var result = Rx.Observable.from(iterator).take(10);
result.subscribe(x => console.log(x));

// Results in the following:
// 3 6 12 24 48 96 192 384 768 1536

fromEvent

创建一个 Observable,该 Observable 发出来自给定事件对象的指定类型事件。

var clicks = Rx.Observable.fromEvent(document, 'click');   
clicks.subscribe(x => console.log(x));   
// 结果:
// 每次点击 document 时,都会在控制台上输出 MouseEvent 。

fromPromise

将 Promise 转化为 Observable。

var result = Rx.Observable.fromPromise(fetch('http://myserver.com/'));
result.subscribe(x => console.log(x), e => console.error(e));

interval

创建一个 Observable ,该 Observable 使用指定的 IScheduler ,并以指定时间间隔发出连续的数字。

每1秒发出一个自增数

var numbers = Rx.Observable.interval(1000);
numbers.subscribe(x => console.log(x));

转换操作符

map

将给定的 project 函数应用于源 Observable 发出的每个值,并将结果值作为 Observable 发出。

类似于大家所熟知的 Array.prototype.map 方法,此操作符将投射函数应用于每个值 并且在输出 Observable 中发出投射后的结果。

// 将每次点击映射为这次点击的 clientX
var clicks = Rx.Observable.fromEvent(document, 'click');
var positions = clicks.map(ev => ev.clientX);
positions.subscribe(x => console.log(x));

take

接收源 Observable 最初的N个值 (N = count),然后完成。

// 获取时间间隔为1秒的 interval Observable 的最初的5秒
var interval = Rx.Observable.interval(1000);
var five = interval.take(5);
five.subscribe(x => console.log(x));

条件和布尔操作符

every

 // 一个简单示例:如果所有元素都小于5就发出 `true`,反之 `false`
 Observable.of(1, 2, 3, 4, 5, 6)
    .every(x => x < 5)
    .subscribe(x => console.log(x)); // -> false

find

只发出源 Observable 所发出的值中第一个满足条件的值。

// 找到并发出第一个点击 DIV 元素的事件
var clicks = Rx.Observable.fromEvent(document, 'click');
var result = clicks.find(ev => ev.target.tagName === 'DIV');
result.subscribe(x => console.log(x));

Scheduler (调度器)

什么是调度器? - 调度器控制着何时启动 subscription 和何时发送通知。它由三部分组成:

  • 调度器是一种数据结构。 它知道如何根据优先级或其他标准来存储任务和将任务进行排序。
  • 调度器是执行上下文。 它表示在何时何地执行任务(举例来说,立即的,或另一种回调函数机制(比如 setTimeout 或 process.nextTick),或动画帧)。
  • 调度器有一个(虚拟的)时钟。 调度器功能通过它的 getter 方法 now() 提供了“时间”的概念。在具体调度器上安排的任务将严格遵循该时钟所表示的时间。

在下面的示例中,我们采用普通的 Observable ,它同步地发出值123,并使用操作符 observeOn 来指定 async 调度器发送这些值。

var observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
})
.observeOn(Rx.Scheduler.async);

console.log('just before subscribe');
observable.subscribe({
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
});
console.log('just after subscribe');

输出结果:

just before subscribe
just after subscribe
got value 1
got value 2
got value 3
done

注意通知 got value...just after subscribe 之后才发送,这与我们到目前为止所见的默认行为是不一样的。这是因为 observeOn(Rx.Scheduler.async)Observable.create 和最终的观察者之间引入了一个代理观察者。在下面的示例代码中,我们重命名了一些标识符,使得其中的区别变得更明显:

var observable = Rx.Observable.create(function (proxyObserver) {
  proxyObserver.next(1);
  proxyObserver.next(2);
  proxyObserver.next(3);
  proxyObserver.complete();
})
.observeOn(Rx.Scheduler.async);

var finalObserver = {
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
};

console.log('just before subscribe');
observable.subscribe(finalObserver);
console.log('just after subscribe');

proxyObserver 是在 observeOn(Rx.Scheduler.async) 中创建的,它的 next(val) 函数大概是下面这样子的:

var proxyObserver = {
  next: (val) => {
    Rx.Scheduler.async.schedule(
      (x) => finalObserver.next(x),
      0 /* 延迟时间 */,
      val /* 会作为上面函数所使用的 x */
    );
  },

  // ...
}

async 调度器操作符使用了 setTimeoutsetInterval,即使给定的延迟时间为0。照例,在 JavaScript 中,我们已知的是 setTimeout(fn, 0) 会在下一次事件循环迭代的最开始运行 fn 。这也解释了为什么发送给 finalObservergot value 1 发生在 just after subscribe 之后。

调度器的 schedule() 方法接收一个 delay 参数,它指的是相对于调度器内部时钟的一段时间。调度器的时钟不需要与实际的挂钟时间有任何关系。这也就是为什么像 delay 这样的时间操作符不是在实际时间上操作的,而是取决于调度器的时钟时间。这在测试中极其有用,可以使用虚拟时间调度器来伪造挂钟时间,同时实际上是在同步执行计划任务。

调度器类型

async 调度器是 RxJS 提供的内置调度器中的一个。可以通过使用 Scheduler 对象的静态属性创建并返回其中的每种类型的调度器。

调度器目的
null不传递任何调度器的话,会以同步递归的方式发送通知。用于定时操作或尾递归操作。
Rx.Scheduler.queue当前事件帧中的队列调度(蹦床调度器)。用于迭代操作。
Rx.Scheduler.asap微任务的队列调度,它使用可用的最快速的传输机制,比如 Node.js 的 process.nextTick() 或 Web Worker 的 MessageChannel 或 setTimeout 或其他。用于异步转换。
Rx.Scheduler.async使用 setInterval 的调度。用于基于时间的操作符。