关于RxJS
RxJS
是 Reactive Extensions for JavaScript
的缩写,起源于 Reactive Extensions
,是一个基于可观测数据流在异步编程应用中的库。它提供了一个核心类型 Observable
,附属类型 (Observer、 Schedulers、 Subjects)
和操作符Operators
,通过使用 observable
序列来编写异步和基于事件的程序。ReactiveX
结合了观察者模式、迭代器模式和使用集合的函数式编程,以满足用一种理想方式来管理事件序列所需要的一切。
一些前置知识点梳理
-
响应式编程(RP —— Reactive Programming)
响应式编程是一种面向数据流和变化传播的编程范式。在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。—— 维基百科- 响应式编程是使用异步数据流进行编程。常见的异步数据流包含事件总线(Event buses)。用包含这些事件在内的任何东西创建数据流(Data stream),监听他并作出响应。
- 只关注业务逻辑互相依赖的事件而不是实现细节。
- 适用于大量和数据有关的事件交互,特别是高实时性要求。
举个例子,React/Vue的设计理念也是一种响应式编程范式,我们在编写代码的过程中,不必关注Dom相关的操作,只需要关注数据的变化。React/Vue会感知到数据的变化,并将这种随着数据变化相应的Dom层的修改自动改变、重新渲染。
-
流(Stream)
作为响应式编程的核心,流的本质是一系列随时间到达的数据。例如:事件流、直播数据流、文本编辑流、WebSocket。举个例子,代码中每1s输出一个数字,用户每一次对元素的点击,就像是在时间这个维度上,产生了一个数据集。这个数据集不像数组那样,它不是一开始都存在的,而是随着时间的流逝,一个一个数据被输出出来。这种异步行为产生的数据,就可以被称之为一个流,在RxJS中,称之为observable(本质上就是一个数据的集合,只是这些数据不一定是一开始就设定好的,而是随着时间而不断产生的)。而RxJS,就是为了处理这种流而产生的工具,比如流与流的合并,流的截断,延迟,消抖等等操作。
-
观察者模式
定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
[图片上传失败...(image-f20eb2-1638425299670)]- 优点:降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系;目标与观察者之间建立了一套触发机制;支持一对多广播通信。
- 缺点:目标与观察者之间的依赖关系并没有完全解除,会出现循环引用;观察层级较深时,广播通信成本高。
-
迭代器模式
在面向对象编程里,迭代器模式是一种设计模式,是一种最简单也最常见的设计模式。迭代器模式可以把迭代的过程从从业务逻辑中分离出来,它可以让用户透过特定的接口访问容器中的每一个元素而不用了解底层的实现。const iterable = [1, 2, 3]; const iterator = iterable[Symbol.iterator](); iterator.next(); // => { value: "1", done: false} iterator.next(); // => { value: "2", done: false} iterator.next(); // => { value: "3", done: false} iterator.next(); // => { value: undefined, done: true}
由上可知Iterator 只有一个 next 方法,通过调用 next 获取值。
作为前端开发者来说,我们最常遇到的部署了Iterator接口的数据结构有:Map、Set、Array、类数组等等,我们在使用他们的过程中,均能使用同一个接口访问每个元素就是运用了迭代器模式。
Iterator作用:- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- 为新的遍历语法 for...of 实现循环遍历
RxJS中的核心概念
-
Observable (可观察对象): RxJS是观察者 + 迭代器模式的结合,Observable作为被观察者,是一个可调用的未来值或事件的集合。它能被多个observer订阅,每个订阅关系相互独立、互不影响。
Observables是使用Rx.Observable.create
或创建操作符创建的,并使用Observer
(观察者)来订阅它,然后执行它并发送 next / error / complete 通知给观察者,而且执行可能会被清理。这四个方面全部编码在Observables
实例中,但某些方面是与其他类型相关的,像 Observer (观察者) 和 Subscription (订阅)。
Observable 的核心关注点:- 创建 Observables:Rx.Observable.create 是 Observable 构造函数的别名,它接收一个参数:subscribe 函数。下面的示例创建了一个 Observable,它每隔一秒会向观察者发送字符串 'hi' 。
var observable = Rx.Observable.create(function subscribe(observer) { var id = setInterval(() => { observer.next('hi') }, 1000); });
- 订阅 Observables
示例中的 Observable 对象 observable 可以订阅,像这样:
observable.subscribe(x => console.log(x));
订阅 Observable 像是调用函数, 并提供接收数据的回调函数。subscribe 调用是启动 “Observable 执行”的一种简单方式, 并将值或事件传递给本次执行的观察者。
- 执行 Observables
Observable.create(function subscribe(observer) {...}) 中...的代码表示 “Observable 执行”,它是惰性运算,只有在每个观察者订阅后才会执行。随着时间的推移,执行会以同步或异步的方式产生多个值。
Observable 执行可以传递三种类型的值:- "Next" 通知:是最重要,也是最常见的类型。发送一个值,比如数字、字符串、对象等等,它们表示传递给观察者的实际数据。- "Error" 通知: 发送一个 JavaScript 错误 或 异常。- "Complete" 通知: 不再发送任何值。
在 Observable 执行中, 可能会发送零个到无穷多个 "Next" 通知。"Error" 和 "Complete" 可能只会发生一次,并且只会执行其中的一个。如果发送的是 "Error" 或 "Complete" 通知的话,那么之后不会再发送任何通知了。
var observable = Rx.Observable.create(function subscribe(observer) { observer.next(1); observer.next(2); observer.next(3); observer.complete(); observer.next(4); // 因为违反规约,所以不会发送 });
Observable 严格遵守自身的规约,所以下面的代码不会发送 "Next" 通知 4。
var observable = Rx.Observable.create(function subscribe(observer) { try { observer.next(1); observer.next(2); observer.next(3); observer.complete(); } catch (err) { observer.error(err); // 如果捕获到异常会发送一个错误 } });
在 subscribe 中可以用 try/catch 代码块来包裹任意代码,如果捕获到异常的话,会发送 "Error" 通知。
- 清理 Observables
因为 Observable 执行可能会是无限的,并且观察者通常希望能在有限的时间内中止执行,所以我们需要一个 API 来取消执行。因为每个执行都是其对应观察者专属的,一旦观察者完成接收值,它必须要一种方法来停止执行,以避免浪费计算能力或内存资源。
当调用了 observable.subscribe ,观察者会被附加到新创建的 Observable 执行中。这个调用还返回一个对象,即 Subscription (订阅)。当你订阅(subscribe)了 Observable,你会得到一个 Subscription ,它表示进行中的执行。只要调用 unsubscribe() 方法就可以取消执行。
var observable = Rx.Observable.from([10, 20, 30]); var subscription = observable.subscribe(x => console.log(x)); // 稍后 subscription.unsubscribe();
-
Observer (观察者): Observable 是多个值的生产者,并将值 push 给观察者,观察者就是由 Observable 发送的值的消费者。观察者只是一组回调函数的集合,每个回调函数对应一种 Observable 发送的通知类型:next、error 和 complete 。
一个典型的观察者对象如下所示:
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 发送的通知类型。
要使用观察者,需要把它提供给 Observable 的 subscribe 方法:
var observable = Rx.Observable.create(function (observer) {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
observable.subscribe(observer);
Subscription (订阅): 表示
Observable
的执行。Subscription
有一个重要的方法,即unsubscribe
,它不需要任何参数,只是用来清理由Subscription
占用的资源主要用于取消Observable
的执行。-
Operators (操作符): 是
Observable
类型上的方法,比如 .map(...)、.filter(...)、.merge(...),等等。当操作符被调用时,它们不会改变已经存在的Observable
实例。相反,它们返回一个新的Observable
,它的subscription
逻辑基于第一个Observable
。
操作符本质上是一个纯函数 (pure function),它接收一个 Observable 作为输入,并生成一个新的 Observable 作为输出。
要解释操作符是如何工作的,文字描述通常是不足以描述清楚的。许多操作符都是跟时间相关的,它们可能会以不同的方式延迟(delay)、取样(sample)、节流(throttle)或去抖动值(debonce)。图表通常是更适合的工具。弹珠图是操作符运行方式的视觉表示,其中包含输入 Obserable(s) (输入可能是多个 Observable )、操作符及其参数和输出 Observable 。
官网操作符:https://cn.rx.js.org/manual/overview.html#h213
弹珠图:https://rxmarbles.com/ -
Subject (主体): RxJS Subject 是一种特殊类型的 Observable,它允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。
- 每个 Subject 都是 Observable:对于 Subject,你可以提供一个观察者并使用 subscribe 方法,就可以开始正常接收值。从观察者的角度而言,它无法判断 Observable 执行是来自普通的 Observable 还是 Subject 。
在下面的示例中,我们为 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 是一个有如下方法的对象: next(v)、error(e) 和 complete() 。要给 Subject 提供新值,只要调用 next(theValue),它会将值多播给已注册监听该 Subject 的观察者们。
因为 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); // observerA: 1 // observerB: 1 // observerA: 2 // observerB: 2 // observerA: 3 // observerB: 3
使用上面的方法,我们基本上只是通过 Subject 将单播的 Observable 执行转换为多播的。这也说明了 Subjects 是将任意 Observable 执行共享给多个观察者的唯一方式。
- 每个 Subject 都是 Observable:对于 Subject,你可以提供一个观察者并使用 subscribe 方法,就可以开始正常接收值。从观察者的角度而言,它无法判断 Observable 执行是来自普通的 Observable 还是 Subject 。
-
Schedulers (调度器): 调度器控制着何时启动 subscription 和何时发送通知。它由三部分组成:
- 调度器是一种数据结构。 它知道如何根据优先级或其他标准来存储任务和将任务进行排序。
- 调度器是执行上下文。 它表示在何时何地执行任务(举例来说,立即的,或另一种回调函数机制(比如 setTimeout 或 process.nextTick),或动画帧)。
- 调度器有一个(虚拟的)时钟。 调度器功能通过它的 getter 方法 now() 提供了“时间”的概念。在具体调度器上安排的任务将严格遵循该时钟所表示的时间。
举例如下:
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
从打印结果上来看,数据的发送时机的确已经由同步变成了异步,如果不进行调度方式修改,那么“just after subscribe”的打印应该是在数据发送完毕之后才会执行的。
RxJS可以解决什么问题
- 同步和异步的统一:RxJS 提供了功能非常强大且复杂的操作符( Operator) 用来处理、组合 Observable,因此 RxJS 拥有十分强大的异步处理能力,几乎可以满足任何异步逻辑的需求,同步逻辑更不在话下。它也抹平了同步和异步之间的鸿沟。
- 可组合的数据变更过程
- 数据和视图的精确绑定:RxJS 的精髓在于推送数据。组件不需要写请求数据和更新数据的两套逻辑,只要订阅一次,就能得到现在和将来的数据,如此一来,就能更好地解耦视图层和数据层的逻辑。视图层从此不用再操心任何有关获取数据和更新数据的逻辑,只要从数据层订阅一次就可以获取到所有数据,从而可以只专注于视图层本身的逻辑。
- 条件变更之后的自动重新计算
RxJS的使用
示例一、控制一秒钟内最多点击一次
// js版本
var count = 0;
var rate = 1000;
var lastClick = Date.now() - rate;
var button = document.querySelector('button');
button.addEventListener('click', () => {
if (Date.now() - lastClick >= rate) {
console.log(`Clicked ${++count} times`);
lastClick = Date.now();
}
});
// Rxjs版本
import { Observable } from 'rxjs';
var button = document.querySelector('button');
Observable.fromEvent(button, 'click')
.throttleTime(1000)
.scan((count: number) => count + 1, 0)
.subscribe(count => console.log(`Clicked ${count} times`));
示例二、控制最大并发数:- 要求最大并发数 maxNum;- 每当有一个请求返回,就留下一个空位,可以增加新的请求;- 所有请求完成后,结果按照 urls 里面的顺序依次打出
import { Observable } from 'rxjs';
// import 'rxjs/add/operator/mergeMap';
// 假设这是你的http请求函数
function httpGet(url: string): Promise {
return new Promise(resolve =>
setTimeout(() => resolve(`Result: ${url}`), 2000)
);
}
const array = [
'https://httpbin.org/ip',
'https://httpbin.org/user-agent',
'https://httpbin.org/delay/3',
'https://httpbin.org/abnet',
'https://httpbin.org/s',
];
// mergeMap是专门用来处理并发处理的rxjs操作符
// mergeMap第二个参数2的意思是,from(array)每次并发量是2,只有promise执行结束才接着取array里面的数据
// mergeMap第一个参数httpGet的意思是每次并发,从from(array)中取的数据如何包装,这里是作为httpGet的参数
const source = Observable.from(array)
.mergeMap(httpGet, 2)
.subscribe(val => console.log(val));
示例三、请求依赖并发:发起一个用户登录请求,成功后再发送3个查询请求(资料,邮件,消息),最后将结果合并输出结果。
import { Observable } from 'rxjs';
const loginRequest = new Promise((resolve, reject) => {
setTimeout(function () {
resolve({sessionId:'xxx-xxx-xxx'})
}, 2000);
});
const queryInfoRequest = function (sessionId) {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve({
id:1,
nickName:'lake'
})
}, 1000)
}
)
};
const queryEmailRequest = function (sessionId) {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve([
{id:1,title:'hi',content:'I miss you'},
{id:2,title:'are you there',content:'how are you doing'}
])
}, 1000)
}
)
};
const queryMessageRequest = function (sessionId) {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve([
{id:1,type:'TEXT',content:'I am ok'},
{id:2,type:'TEXT',content:'yeah'}
])
}, 1000)
}
)
};
const oldTime = new Date().getTime();
Observable.fromPromise(loginRequest)
.flatMap(sessionId=>{
return [
Observable.fromPromise(queryInfoRequest(sessionId)),
Observable.fromPromise(queryEmailRequest(sessionId)),
Observable.fromPromise(queryMessageRequest(sessionId)),
];
})
.combineAll()
.subscribe(value => {
console.log(value);
console.log("总运行时长:"+(new Date() - oldTime));
});
总结
- 异步操作和同步操作最大的区别就是异步有时序。我们可以把同步操作理解为:数据+函数;那么异步操作就是:数据+函数+时序。RxJS做的事情就是把时序抽离成一根时间轴,在这根时间轴上进行同步操作,而异步相关的时序处理就交给Rx提供的各种operator。所以如果你的应用是一个时序密集的应用,那么使用Rx能帮你理清楚复杂的异步逻辑。反之,如果异步操作之间没有太多的联系,时序简单,则不那么需要使用RxJS。
- RxJS 最核心的地方也不是一堆花哨的操作符,而是它的响应式的思想,它是能改变你整个编程思路的东西。如果仅仅是用 RxJS 来解决应用中的一些孤立的异步场景,比如拖拽、节流之类的,用 RxJS 是有点大材小用了。因为你不用 RxJS 也可以实现,顶多就是没有 RxJS 优雅而已。最能够体现 RxJS 威力的还是那些有大量的异步数据更新,数据之间还互相有依赖关系的大型前端应用。这种场景下,你甚至可以基于 RxJS 设计一整套数据管理的方案,当然也不要给本不适合RxJS理念的场景强加使用,这样实际带来的效果可能并不明显。