背景
本来计划分享的主题本来是reactive programming,即一种编程范式。因关于编程范式想讲的内容过多,本次先就其中一个具体实现,即ReactiveX入手,后面有机会可以再展开。
ReactiveX
包含各个语言的实现,其中概念基本一致,具体的介绍中会以rxjs为例。
为什么要用
ReactiveX
用来处理各种数据流,其中流是按顺序发生的离散事件,比如点击事件,比如网络请求回调,比如定时器回调等。有了这个工具我们处理异步事件流就像处理普通数组一样简单,从而简化了代码、可读性更强以及提高代码稳定性。
** 注意响应式编程里一切都是流,要想应用这种思想,需要先把普通数据转化为流。**
前端为了处理异步,虽然引入了 Promise,乃至 async/await 的语法糖,但是对一些场景处理起来仍然很繁琐,比如竞态,由于切换 tab 或者下拉框发出多个请求,先发生的请求后返回就会造成页面数据渲染错误
利用现有解决方案,需要取消无效且未完成的请求或者将请求标记和结果匹配,比如使用 AbortController记录前一个请求,当发起新请求时将上一个请求取消。
再比如拖拽等复杂的事件处理。
具体介绍
ReactiveX
为了解决数据流,提供了一个核心类型Observable
以及其他相关类型和方法。
其中 Observable
表示一个数据流的生产者,可以被 Observer
使用 subscribe
监听,Observer
是一个数据流的消费者,包含对数据不同情况(数据正常到达 next、数据流完成 complete 和出错 error)的回调,默认表示 next。
当数据流产生新数据时会执行 Observer
中定义的回调,这点类似于观察者模式(比如dom事件监听),不过和观察者模式不同的是,只有被监听的 Observable
才会开始执行,这一点类似于函数。
import { Observable } from "rxjs";
const observable = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 1000);
});
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");
//Logs
//just before subscribe
//got value 1
//got value 2
//got value 3
//just after subscribe
//got value 4
//done
Observable
实例调用subscribe
后返回一个subscription
,也称作Disposable
,可以用来取消监听。
以上出现的数据源都是单播的,为了使用广播我们可以使用Subject
,它
比如
import { Subject, from } from 'rxjs';
const subject = new Subject<number>();
subject.subscribe({
next: (v) => console.log(`observerA: ${v}`)
});
subject.subscribe({
next: (v) => console.log(`observerB: ${v}`)
});
const observable = from([1, 2, 3]);
observable.subscribe(subject); // You can subscribe providing a Subject
// Logs:
// observerA: 1
// observerB: 1
// observerA: 2
// observerB: 2
// observerA: 3
// observerB: 3
如果想对数据流的发生时机更细的控制,可以显式引入Scheduler
。 .
Observable
Observable
在tc39处于提案阶段,有望被 js 标准化 (目前进展不大)。
为了更好的理解Observable
,我们将其与其他概念做一下对比。
单值 | 多值 | |
---|---|---|
Pull(同步) | Function | Iterator |
push (异步) | Promise | Observable |
push
和pull
是两个表述数据生产者和消费者通信的协议,如果生产者决定数据发送的时机,即 push,否则就是 pull,实质上也可以用异步和同步来表示。
不过 Observable 不仅仅能处理异步,还能处理同步,不过不是很有必要。
Operators
前面介绍了数据流的生产和消费过程,但ReactiveX
的强大作用需要一系列 operators 来完成。
operators 是一系列函数,包括两类
- Creation Operators 用来创建 Observable,比如将数组或事件等转化为数据流
import { from } from "rxjs";
const array = [10, 20, 30];
const result = from(array);
result.subscribe((x) => console.log(x));
// Logs:
// 10
// 20
// 30
import { fromEvent } from "rxjs";
const clicks = fromEvent(document, "click");
clicks.subscribe((x) => console.log(x));
// Results in:
// MouseEvent object logged to console every time a click
// occurs on the document.
- Pipeable Operators 即函数式编程中的纯函数,输入一个 Observable,生成一个新的 Observable,用来以旧的流为基础生成新的流,语法是
observableInstance.pipe(operator())
,如
import { of } from "rxjs";
import { map } from "rxjs/operators";
of(1, 2, 3)
.pipe(map((x) => x * x))
.subscribe((v) => console.log(`value: ${v}`));
// Logs:
// value: 1
// value: 4
// value: 9
类似于
[1, 2, 3].map((x) => x * x);
多个 Operators 表示成
obs.pipe(op1(), op2(), op3(), op4());
//相当于
op4()(op3()(op2()(op1()(obs))));
通常使用表示 operators 的工作方式
--a---b-c---d---X---|->
a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
更多 operator 的表示看这里
有什么应用
当使用ReactiveX
时要记住一句话,一切皆流
,要把一切数据源先转化成流的形式再来处理。
比如要解决竞态的问题,可以将每个请求作为一个流,当新的流到来时就将旧流抛弃,这时候应使用switchAll,比如下面,完整代码看这里
const [text, setText] = useObservableState<
PokeItem[],
React.ChangeEvent<HTMLInputElement>
>((e) =>
e.pipe(
pluckCurrentTargetValue,
filter((val) => {
console.log(val)
return val.length > 0
}),
distinctUntilChanged(),
switchMap((val) => from(getPokemonByName(val)))
)
)
另外一个场景是导出 excel,即使用不同页码参数依次去请求所有符合条件的数据,然后处理成 excel 导出所需要的格式进行下载。如果使用流的思路处理,即先生成所有请求所需要的参数数组,然后将对应的请求转化为流,等所有 Obversable 完成再进行格式转换实现导出,其中通过mergeAll设置最大并发量,即能保证下载速度,又能避免造成服务器过多压力,比如
//生成Obverable组成的数组
const observables = new Array(restrict)
.fill(0)
.map((_, index) =>
defer(() => getData(generateParams(index + 1)).unwrap())
)
//如果导出需要顺序,则mergeAll参数需要为1,否则导出顺序按照请求到达的先后顺序
from(observables)
.pipe(
mergeAll(5),
scan((pre, cur) => {
return pre.concat(cur.list)
}, []),
last()
)
.subscribe({
error() {},
next(dataSource) {
doDownload(firstRes.list.concat(dataSource))
},
})
}
最后重复一遍reactiveX的使用要点:一切都是流。