angular - Rxjs
- 安装#
- 不用构建工具,CDN调试#
- Rxjs基础#
- Subject 构造#
- 操作符#
- 创建操作符#
- range 指定范围内的数字序列#
- from (ObservableInput) 一切转为 Observable#
- of 根据提供的参数创建 Observable#
- fromEvent 发出来自给定事件对象的指定类型事件#
- fromEventPattern 添加和删除事件处理器#
- interval 定期发出自增的数字#
- timer 指定时间的 interval#
- generate 循环创建#
- repeat 重复 count 次#
- repeatWhen 重复 notifier 的吞吐时间#
- 转换操作符#
- 映射数据#
- 缓存窗口:无损回压控制 (用到再查)#
- 高阶 map#
- concatMap = concatAll + map#
- mergeMap = mergeAll + map#
- swtichMap 切换可观察对象#
- exhaustMap = map + exhaustAll#
- 数据分组#
- 累计数据#
- 过滤操作符#
- take#
- takeWhile#
- takeUntil#
- First#
- skip 跳过#
- distinct 每个重复元素过滤#
- distinctUntilChanged 最后元素重复比较#
- throttleTime 限流#
- debounceTime 防抖#
- distinctUntilChanged 检测数据流是否和上次相同#
- 组合操作符#
- forkjoin 合并多个请求,直到发出的最后一个值#
- startWith 使用前加塞#
- concat 串联#
- concatAll 高阶 Observable 转化为一阶 Observable#
- merge 并联#
- combineLatest 多输入操作符,组合多个observable 的最新值#
- withLatestFrom 单输入操作符#
- zip:拉链式组合#
- race 胜者通吃#
- 高阶 Observable#
- 辅助类操作符#
- Rxjs 代码演示#
- Rxjs 实时弹珠图#
# 安装#
1 先添加一个 package.json
npm init -y
2 使用命令 npm install rxjs
1.查看当前源
npm config get registry
更换npm源为国内淘宝镜像
npm config set registry https://registry.npmmirror.com/还原npm源
npm config set registry https://registry.npmjs.org/
不用构建工具,CDN调试#
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/rxjs@7.8.1/dist/bundles/rxjs.umd.min.js"></script>
<script type="module" src="./index.js"></script>
</head>
<body></body>
</html>
index.js
// import { Observable, from, of } from 'rxjs'; //写代码
const { Observable, from, of } = rxjs; //网页调试
Rxjs基础#
//Observable(可观察者):表示未来(future)值或事件的可调用集合的概念。
const observable = new Observable((subscriber) => {
setTimeout(() => {
subscriber.next({ name: '张三' });
subscriber.complete();
}, 2000);
});
// Observer(观察者):是一个回调集合,它知道如何监听 Observable 传来的值。
const observer = {
next: function (x: any) {
console.log(x);
}
}
// Subscription(订阅):表示 Observable 的一次执行,主要用于取消执行。
observable.subscribe(observer);
console.log('just after subscribe');
可观察对象(Observable)/观察者(Observer)/订阅(Subscribe)#
(1)可多次调用next
发送数据
//Observable(可观察者):表示未来(future)值或事件的可调用集合的概念。
const observable = new Observable((subscriber) => {
setTimeout(() => {
subscriber.next({ name: '张三' });
subscriber.complete();
}, 2000);
});
// Observer(观察者):是一个回调集合,它知道如何监听 Observable 传来的值。
const observer = {
next: function (x: any) {
console.log(x);
}
}
// Subscription(订阅):表示 Observable 的一次执行,主要用于取消执行。
observable.subscribe(observer);
console.log('subscribe 之后');
(2)当完成使用 complete
结束调用
//Observable(可观察者):表示未来(future)值或事件的可调用集合的概念。
const observable = new Observable((subscriber) => {
let index = 0;
let timer = setInterval(() => {
subscriber.next(index++);
if (index == 5) {
subscriber.complete();
clearInterval(timer);
}
}, 1000);
});
// Observer(观察者):是一个回调集合,它知道如何监听 Observable 传来的值。
const observer = {
next: function (x: any) {
console.log(x);
},
complete: () => {
console.log('完成');
}
}
// Subscription(订阅):表示 Observable 的一次执行,主要用于取消执行。
observable.subscribe(observer);
console.log('subscribe 之后');
(3)error
:内部逻辑错误,将失败信息发给订阅者。Observerable 终止。
//Observable(可观察者):表示未来(future)值或事件的可调用集合的概念。
const observable = new Observable((subscriber) => {
let index = 0;
let timer = setInterval(() => {
subscriber.next(index++);
if (index == 5) {
// subscriber.complete();
subscriber.error("失败了~");
clearInterval(timer);
}
}, 1000);
});
// Observer(观察者):是一个回调集合,它知道如何监听 Observable 传来的值。
const observer = {
next: function (x: any) {
console.log(x);
},
error: (x: string) =>{
console.log(x);
},
complete: () => {
console.log('完成');
}
}
// Subscription(订阅):表示 Observable 的一次执行,主要用于取消执行。
observable.subscribe(observer);
console.log('subscribe 之后');
退订 unsubscribe#
现在已经了解了Observable和Observer之间如何建⽴关系, 两者之间除了要通过subscribe建⽴关系, 有时候还需要断开两者的关系, 例如,Observer只需要监听⼀个Observable对象三秒钟时间, 三秒钟之后就不关⼼ 这个Observable对象的任何事件了, 这时候怎么办呢
const onSubscribe = (observer) => {
let number = 1;
const handle = setInterval(() => {
observer.next(number++);
}, 1000);
return {
unsubscribe: () => {
console.log("do unsubscribe");
clearInterval(handle);
},
};
};
const source$ = new Observable(onSubscribe);
const theObserver = {
next: (item) => console.log(item),
complete: () => console.log("no more data"),
};
const subscription = source$.subscribe(theObserver);
setTimeout(() => {
subscription.unsubscribe();
}, 3500);
// 结果
// (间隔1秒)
// 1
// (间隔1秒)
// 2
// (间隔1秒)
// 3
// do unsubscribe
Subject 构造#
订阅立即执行 | 传参 | 广播历史 | |
---|---|---|---|
Subject | 否 | 否 | 否 |
BehaviorSubject | ✔ | ✔ | 否 |
ReplaySubject | 否 | 否 | ✔ |
用于创建可观察对象,但订阅后不会立刻执行,next 可以在可观察对象外部调用
const demoSubject = new Subject()
demoSubject.subscribe({
next: (x: any) => {
console.log(x);
}
})
demoSubject.subscribe({
next: (x: any) => {
console.log(x);
}
})
console.log('没调用');
setTimeout(() => {
demoSubject.next("222") //2秒后调用
}, 2000);
BehaviorSubject:可传参的 Subject
,而且立即调用#
const behaviorSubject = new BehaviorSubject('默认值~');
behaviorSubject.subscribe({
next: (x: any) => {
console.log(x);
}
})
behaviorSubject.next('改变值');
ReplaySubject:它通过在新订阅者首次订阅时发送旧值来“重播”旧值。#
ReplaySubject
会广播历史结果,而Subject
不会广播历史结果
import { ReplaySubject } from 'rxjs';
const replaySubject = new ReplaySubject();
replaySubject.subscribe({
next: (x: any) => {
console.log(x);
}
})
replaySubject.next('hello 1')
replaySubject.next('hello 2')
setTimeout(() => {
//过2秒再订阅
replaySubject.subscribe({
next: (x: any) => {
console.log(x);
}
})
}, 3000);
操作符#
操作符是 Observable 类型上的方法,比如 .map(...)
、.filter(...)
、.merge(...)
,等等。当操作符被调用时,它们不会改变已经存在的 Observable 实例。相反,它们返回一个新的 Observable ,它的 subscription 逻辑基于第一个 Observable 。
操作符是函数,它基于当前的 Observable 创建一个新的 Observable。这是一个无副作用的操作:前面的 Observable 保持不变。
创建操作符#
range 指定范围内的数字序列#
range(start: number, count: number, scheduler: Scheduler): Observable
创建一个 Observable ,它发出指定范围内的数字序列。
import { range } from 'rxjs';
range(0,10).subscribe((x)=>console.log(x))
from (ObservableInput) 一切转为 Observable#
从一个 ObservableInput (数组、类数组对象、Promise、迭代器对象或者类 Observable 对象) 创建一个 Observable.
from([10, 20, 30]).subscribe(x => console.log(x));
//10
//20
//30
of 根据提供的参数创建 Observable#
发出你提供的参数,然后完成。
of([1, 2, 3]).subscribe(x => console.log(x));
//打印 [1,2,3]
of('a',1,[1, 2, 3]).subscribe(x => console.log(x));
//'a'
//1
//[1, 2, 3]
fromEvent 发出来自给定事件对象的指定类型事件#
创建一个 Observable,该 Observable 发出来自给定事件对象的指定类型事件。
通过给“事件目标”添加事件监听器的方式创建 Observable,可能会是拥有addEventListener
和 removeEventListener
方法的对象,一个 Node.js 的 EventEmitter,一个 jQuery 式的 EventEmitter, 一个 DOM 的节点集合, 或者 DOM 的 HTMLCollection。 当输出 Observable 被订阅的时候事件处理函数会被添加, 当取消订阅的时候会将事件处理函数移除。
名称 | 类型 | 属性 | 描述 |
---|---|---|---|
target | EventTargetLike | DOMElement, 事件目标, Node.js EventEmitter, NodeList 或者 HTMLCollection 等附加事件处理方法的对象。 | |
eventName | string | 感兴趣的事件名称, 被 target 发出。 | |
options | EventListenerOptions | - 可选的 | 可选的传递给 addEventListener 的参数。 |
selector | SelectorMethodSignature | - 可选的 | 可选的函数处理结果. 接收事件处理函数的参数,应该返回单个值。 |
fromEvent(document, 'click').subscribe(x => console.log(x));
// 结果:
// 每次点击 document 时,都会在控制台上输出 MouseEvent 。
angular
html
<button id="btn">提交</button>
app.component.ts
ngOnInit() {
const btn = document.getElementById('btn')
if (btn != null) {
fromEvent(btn, 'click')
.pipe(map(event => event.target))
.subscribe(console.log);
}
}
fromEventPattern 添加和删除事件处理器#
通过使用addHandler
和 removeHandler
添加和删除事件处理器, 使用可选的选择器函数将事件参数转化为结果.
addHandler
当输出 Observable 被订阅的时候调用,removeHandler
方法在取消订阅的时候被调用
fromEventPattern<T>(
addHandler: (handler: NodeEventHandler) => any,
removeHandler?: (handler: NodeEventHandler, signal?: any) => void,
resultSelector?: (...args: any[]) => T
): Observable<T | T[]>
addHandler | function(handler: Function): any | 一个接收处理器的函数,并且将 该处理器添加到事件源。 | |
---|---|---|---|
removeHandler | function(handler: Function, signal?: any): void | - 可选的 | 可选的 函数,接受处理器函数做为参数,可以移除处理器当之前使用addHandler 添加处理器。如果 addHandler 返回的信号当移除的时候要清理,removeHandler 会去做这件事情。 |
selector | function(...args: any): T | - 可选的 | 可选的函数处理结果。 接受事件处理的参数返 回单个的值。 |
发出 DOM document 上的点击事件
function addClickHandler(handler) {
document.addEventListener('click', handler);
}
function removeClickHandler(handler) {
document.removeEventListener('click', handler);
}
var clicks = fromEventPattern(
addClickHandler,
removeClickHandler
);
clicks.subscribe(x => console.log(x));
interval 定期发出自增的数字#
每隔一段时间发送数值,数值递增
interval(1000).subscribe(console.log)
timer 指定时间的 interval#
timer(initialDelay: number | Date, (可选)period: number, (可选)scheduler: Scheduler): Observable
名称 | 类型 | 属性 | 描述 |
---|---|---|---|
initialDelay | number \ | Date | |
period | number | - 可选的 | 连续数字发送之间的时间周期。 |
scheduler | Scheduler | - 可选的 - 默认值: async |
调度器,用来调度值的发送, 提供“时间”的概念。 |
就像是interval, 但是你可以指定什么时候开始发送
timer(3000,1000).subscribe(console.log) //像interval,但是这里可以设置过3秒
generate 循环创建#
generate(
2, // 初始值,相当于for循环中的i=2
value => value < 10, //继续的条件,相当于for中的条件判断
value => value + 2, //每次值的递增
value => value // 产生的结果
);
repeat 重复 count 次#
repeat(count: number): Observable
repeatWhen 重复 notifier 的吞吐时间#
repeatWhen
接受一个函数作为参数,这个函数在上游第一次产生异常时被调用,然后这个函数应该返回一个Observable对象,这个对象就是一个控制器,作用就是控制repeatWhen何时重新订阅上游,当控制器Observable吐出一个数据的时候,repeatWhen就会做退订上游并重新订阅的动作
repeatWhen(notifier: function(notifications: Observable): Observable): Observable
notifier
: 接收 Observable 的通知,用户可以该通知 的 complete
或error
来中止重复
notifications
: 这个参数也是一个Observable对象,每当repeatWhen上游完结的时候,这个notificaton$就会吐出一个数据
根据 notifier 的吞吐时间,重新订阅
const notifier = () => interval(1000);
of(1,2,3).pipe(repeatWhen(notifier)).subscribe((data) => console.log(data));
如果repeatWhen的上游并不是同步产生数据,完结的时机也完全不能确定,如果想要每次在上游完结之后重新订阅,那使用interval来控制重新订阅的节奏就无法做到准确了,这时候就需要用到notifier函数的参数
每隔2秒重新订阅一次
const notifier = (notifications$:any) =>{
return notifications$.pipe(delay(2000));
}
of(1,2,3).pipe(repeatWhen(notifier)).subscribe((data) => console.log(data));
转换操作符#
转换分为2类
-
每个数据转换。
上游的数据和下游的数据依然是一对一的关系,只不过传给下游的数据已经是另一个数据,比如上游传下来的是数据A,传给下游的是数据f(A),其中f是一个函数,以A为输入返回一个新的数据
-
不转换单个数据,而是把数据重新整合。
比如上游传下来的是A、B、C三个数据,传给下游的是一个数组数据[A, B, C],并没有改变上游数据本身,只是把它们都塞到了一个数组对象中传给下游
映射数据#
map 数据转换#
基于数据流,进行数据转换。
import { map, range } from 'rxjs';
range(0,10)
.pipe(map(x=>x*10))
.subscribe((x)=>console.log(x))
mapTo 每个数据映射为一个值#
将一个常量 value
作为参数,并且每当源 Observable 上发来一个值时就发送该值。换句话说,忽略实际的源值,并单纯根据发送时机来决定何时发送给定的 value
。
例子:将每次点击都映射为字符串 'Hi'
const clicks = fromEvent(document, 'click');
const greetings = clicks.pipe(mapTo('Hi'));
greetings.subscribe(x => console.log(x));
pluck 提取每个数据的某个字段(v8要弃用)#
使用 map
和可选链接: pluck('foo', 'bar')
is map(x => x?.foo?.bar)
。将在 v8 中删除。
获取属性值
ngOnInit() {
const btn = document.getElementById('btn')
if (btn != null) {
fromEvent(btn, 'click')
// .pipe(map(event => event.target))
.pipe(pluck('target'))
.subscribe(console.log);
}
}
例子2:pluck也有优点,就是能够自动处理字段不存在的情况,比如,如果访问并不存在的嵌套属性
const result$ = source$.pipe(pluck('nosuchfield', 'foo'));
每一个上游数据并没有nosuchfield
这个字段,直接访问nosuchfield.foo
的话肯定会出错,如果用map来实现就必须要考虑到nosuchfield为空的情况,但是如果使用pluck则不用考虑,pluck发现某一层字段为空,对应就会给下游传递undefined
,不会出错。
缓存窗口:无损回压控制 (用到再查)#
无损的回压控制就是把上游在一段时间内产生的数据放到一个数据集合里,然后把这个数据集合一次丢给下游。在这里所说的“数据集合”,可以是一个数组,也可以是一个Observable对象,RxJS有两组操作符对两种数据集合类型分别提供支持,支持数组的以buffer开头,支持Observable对象的以window开头。
接下来介绍RxJS提供的无损回压控制操作符,将上游数据放在数组中传给下游的操作符都包含buffer这个词,包括:
❑ bufferTime
❑ bufferCount
❑ bufferWhen
❑ bufferToggle
❑ buffer
此外,将上游数据放在Observable中传给下游的操作符都包含window这个词,包括:
❑ windowTime
❑ windowCount
❑ windowWhen
❑ windowToggle
❑ window
这两组操作符完全意义对应,所以只要理解了其中一组,就可以明白另一组的功能,区别只在于传给下游的数据集合形式。
高阶 map#
concatMap = map + concatAll
mergeMap = map + mergeAll
switchMap = map + switch
exhaustMap = map + exhaust
所有高阶map的操作符都有一个函数参数project,但是和普通map不同,普通map只是把一个数据映射为另一个数据,而高阶map的函数参数project把一个数据映射为一个Observable对象。下面是一个这样的project函数例子
const project = (value: number, index: number) =>
interval(100).pipe(take(5));
roject函数有两个参数,第一个参数value就是高阶map上游传下来的数据,第二个参数index是对应数据在上游数据流中的序号。在这个例子中,为了简单起见,并没有使用这两个参数,而是直接返回一个interval和take产生的Observable对象,间隔100毫秒依次产生数据0、1、2、3、4
const source$ = interval(200).pipe(map(project));
可以看到,在这里map产生的是一个高阶Observable对象,project返回的结果成为这个高阶Observable对象的每个内部Observable对象。所谓高阶map,所做的事情就是比普通的map更进一步,不只是把project返回的结果丢给下游就完事,而是把每个内部Observable中的数据做组合,通俗一点说就是“砸平”,最后传给下游的依然是普通的一阶Observable对象
concatMap = concatAll + map#
将每个值映射到一个 Observable,然后使用 concatAll
展平所有这些内部 Observable。
例子1 前面的 map 换成 concatMap
const project = (value: number, index: number) =>
interval(100).pipe(take(5));
const source$ = interval(200).pipe(concatMap(project));
source$.subscribe(console.log);
concatMap适合处理需要顺序连接不同Observable对象中数据的操作,有一个特别适合使用concatMap的应用例子,就是网页应用中的拖拽操作
例子2:对于每个点击事件,每秒会依次发出 0 到 3,非并发
import { fromEvent, concatMap, interval, take } from 'rxjs';
const clicks = fromEvent(document, 'click');
const result = clicks.pipe(
concatMap(ev => interval(1000).pipe(take(4)))
);
result.subscribe(x => console.log(x));
// Results in the following:
// (results are not concurrent)
// For every click on the "document" it will emit values 0 to 3 spaced
// on a 1000ms interval
// one click = 1000ms-> 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3
mergeMap = mergeAll + map#
将每个值映射到一个 Observable,然后使用 mergeAll
展平所有这些内部 Observable。
将每个字母映射为一个每秒发送一个条目的 Observable 并展平
const letters = of('a', 'b', 'c');
const result = letters.pipe(
mergeMap(x => interval(1000).pipe(map(i => x + i)))
);
result.subscribe(x => console.log(x));
// Results in the following:
// a0
// b0
// c0
// a1
// b1
// c1
// continues to list a, b, c every second with respective ascending integers
mergeMap能够解决异步操作的问题,最典型的应用场景就是对于AJAX请求的处理。
在一个网页应用中,一个很典型的场景,每点击某个元素就需要发送一个AJAX请求给服务器端,同时还要根据返回结果更新网页中的状态,AJAX的处理当然是一个异步过程,使用传统的方法来解决这样的异步过程代码会十分繁杂。但是,如果把用户的点击操作看作一个数据流,把AJAX的返回结果也看作一个数据流,那么这个问题的解法就是完全另一个样子,可以非常简洁,下面是示例代码:
const sendButton = document.querySelector('#send');
fromEvent(sendButton, 'click').pipe(mergeMap(() => {
return ajax(apiUrl);
})).subscribe(result => {
// 正常处理AJAX返回的结果
});
虽然是一个异步操作,但是整个代码依然是同步的感觉,这就是RxJS的优势。
swtichMap 切换可观察对象#
它仍然提供一个Observable作为输出,不是通过合并,而是通过仅从最新的Observable 发出的结果。
重点突出在切换最后1次Observeble.
后产生的内部Observable对象优先级总是更高,只要有新的内部Observable对象产生,就立刻退订之前的内部Observable对象,改为从最新的内部Observable对象拿数据
const btn = document.getElementById('btn')
if (btn != null) {
fromEvent(btn, 'click')
.pipe(switchMap(event => interval(1000)))
.subscribe(console.log);
}
对于我们的最后一个示例,如果我们使用switchMap,我们只会从最后一个Observable 中获取结果。
const dymiAPI = (character:any): any => {
return of(`API response for character: ${character}`).pipe(delay(1000))
}
from(['aa', 'bb', 'cc', 'dd']).pipe(
switchMap(arr => dymiAPI(arr))
).subscribe(data => console.log(data)) //这里只会输出最新的 dd
//结果:
//API response for character: dd
exhaustMap = map + exhaustAll#
数据的处理策略和switchMap正好相反,先产生的内部Observable优先级总是更高,后产生的内部Observable对象被利用的唯一机会,就是之前的内部Observable对象已经完结。
exhaustAll
通过在当前内部 Observable 仍在执行时丢弃其它内部 Observables 来展平某个发送 Observables 的 Observable。
数据分组#
groupBy#
根据指定条件将源 Observable 发出的值进行分组,并将这些分组作为 GroupedObservables
发出,每一个分组都是一个 GroupedObservable 。
例子1:按 id
对一些对象进行分组并以数组形式返回
of(
{ id: 1, name: 'JavaScript' },
{ id: 2, name: 'Parcel' },
{ id: 2, name: 'webpack' },
{ id: 1, name: 'TypeScript' },
{ id: 3, name: 'TSLint' }
).pipe(
groupBy(p => p.id),
mergeMap(group$ => group$.pipe(reduce((acc, cur) => [...acc, cur], [])))
)
.subscribe(p => console.log(p));
// displays:
// [{ id: 1, name: 'JavaScript' }, { id: 1, name: 'TypeScript'}]
// [{ id: 2, name: 'Parcel' }, { id: 2, name: 'webpack'}]
// [{ id: 3, name: 'TSLint' }]
例子2:在 id
字段上透视数据
of(
{ id: 1, name: 'JavaScript' },
{ id: 2, name: 'Parcel' },
{ id: 2, name: 'webpack' },
{ id: 1, name: 'TypeScript' },
{ id: 3, name: 'TSLint' }
).pipe(
groupBy(p => p.id, { element: p => p.name }),
mergeMap(group$ => group$.pipe(reduce((acc, cur) => [...acc, cur], [`${ group$.key }`]))),
map(arr => ({ id: parseInt(arr[0], 10), values: arr.slice(1) }))
)
.subscribe(p => console.log(p));
// displays:
// { id: 1, values: [ 'JavaScript', 'TypeScript' ] }
// { id: 2, values: [ 'Parcel', 'webpack' ] }
// { id: 3, values: [ 'TSLint' ] }
在网页应用中,可以用groupBy来对DOM事件进行分组,例如,我们需要对不同class的元素的点击事件做不同处理,HTML中可能包含下面的结构
<button class="foo">Button One</button>
<button class="foo">Button Two</button>
<button class="bar">Button Three</button>
因为每一种class的元素可能都有多个,如果对每一个元素都用fromEvent获取一次数据流,那就太麻烦了。一个比较好的方法是利用DOM事件的冒泡功能,也就是对所有DOM元素的点击事件,同时也是对这些DOM元素的父元素的点击事件,也是父元素的父元素的点击事件,依次往上,当然也是对document的点击事件,于是,代码可以这么写
const click$ = fromEvent(document, 'click');
const groupByClass$ = click$.groupBy(event => event.target.className);
groupByClass$.filter(value => value.key === 'foo')
.mergeAll()
.subscribe(fooEventHandler);
groupByClass$.filter(value => value.key === 'bar')
.mergeAll()
.subscribe(barEventHandler);
partition 简单分2组#
将源 Observable 拆分为两个,一个拥有满足此谓词的值,另一个拥有不满足此谓词的值。
partition接受一个判定函数作为参数,对上游的每个数据进行判定,满足条件的放一个Observable对象,不满足条件的放到另一个Observable对象,就这样一分二。有意思的是,partition是RxJS提供的操作符中唯一的不返回Observable对象的操作符,它返回的是一个数组,包含两个元素,第一个元素是容纳满足判定条件的Observable对象,第二个元素自然是不满足判定条件的Observable对象
对于很多具体问题,使用groupBy显得是牛刀杀鸡,比如上游数据是整数序列,需要把奇数和偶数分组处理,如果用groupBy的话,产生的高阶Observable中也无法确定第一个Observable是代表奇数还是第二个Observable是代表奇数,因为这完全取决于上游是先出现奇数还是偶数,而且,实际上我们只需要产生两个Observable对象,但是却不得不去处理一个高阶Observable对象。RxJS提供的partition就能简化这样问题的处理,对于需要把一个Observable对象分为两个Observable对象的操作,partition比groupBy更直观更易用。
const observableValues = of(1, 2, 3, 4, 5, 6);
const [evens$, odds$] = partition(observableValues, value => value % 2 === 0);
odds$.subscribe(x => console.log('odds', x));
evens$.subscribe(x => console.log('evens', x));
累计数据#
前面介绍的所有操作符,上游的数据之间不会产生任何影响。比如,上游产生一个数据A,经过操作符处理,可能变成了另一个数据f(A),也可能被放到了另一个Observable对象中或者一个数组中,这个数据A被操作符处理完之后也就完了,不会对后续的数据产生什么影响;如果上游再产生一个数据B,传给下游的是一个f(B),或者B被放到一个Observable对象或者数组中,就好像之前并不曾有一个数据A存在过一样,因为各个数据之间是独立的。当然,像上面这样各个数据之间毫无关系的处理,非常符合函数式编程的原则,各个数据之间纠葛越少,程序的逻辑也就越简单清晰,产生bug的可能性也就越少。但是,某些应用场景下,传给下游的数据依赖于之前产生的所有数据。对应上面的例子,当上游产生数据B时,希望传给下游的不是f(B),而是f(A, B),也就是希望传给下游的数据是一个综合了之前上游数据A的函数f执行结果,当然,如果上游再产生了一个数据C,希望由此给下游传递一个综合之前所有数据的结果f(A, B,C)。为了实现这种功能,RxJS提供了能够累计所有上游数据的操作符,例如scan和mergeScan。
scan 累计所有上游数据的操作符(相对于reducer)#
用于封装和管理状态。在使用 seed
值(第二个参数)或来自源的第一个值建立了初始状态之后,对来自源的每个值调用累加器(或“reducer 函数”)
accumulator | 一个“归结器函数”。这将在获得初始状态后针对每个值进行调用。 |
---|---|
seed | 可选。默认值为 undefined 。初始状态。如果未提供,则会把源中的第一个值用作初始状态,并在不经过累加器的情况下发送。则后续的所有值都会由累加器函数处理。如果提供了它,则所有值都会传给累加器函数。 |
scan和reduce的区别在于scan对上游每一个数据都会产生一个规约结果,而reduce是对上游所有数据进行规约,reduce最多只给下游传递一个数据,如果上游数据永不完结,那reduce也永远不会产生数据,而scan完全可以处理一个永不完结的上游Observable对象
const source$ = interval(100);
const result$ = source$.pipe(scan((accumulation, value) => {
return accumulation + value;
}));
result$.subscribe(console.log)
该操作符会维护一个内部状态,并在处理每个值后发送它,如下所示:
- 第一个值抵达
如果提供了
seed
值(作为scan
的第二个参数),则让state = seed
和value = firstValue
。如果没有提供
seed
值(没有第二个参数),则让state = firstValue
并转到 3。
- 让
state = accumulator(state, value)
。
- 如果
accumulator
抛出错误,则向使用者通知一个错误。该过程结束。
发送
state
。下一个值抵达,让
value = nextValue
,转到 2。
下面来输出一下看看情况
const numbers$ = of(1, 2, 3);
numbers$
.pipe(
// Get the sum of the numbers coming in.
scan((total, n) => {console.log(`scan:total:${total},n:${n},total + n=${total + n}`); return total + n}),
)
.subscribe(console.log);
//1
//scan:total:1,n:2,total + n=3
//3
//scan:total:3,n:3,total + n=6
//6
过滤操作符#
take#
获取最前的N个
interval(1000).pipe(take(2)).subscribe(console.log)
takeWhile#
满足 predicate
函数的每个值,并且一旦出现不满足 predicate
的值就立即完成
range(1, 10).pipe(takeWhile(x => x < 5)).subscribe(console.log) //1,2,3,4
fromEvent(document, 'mousemove')
.pipe( takeWhile((Event: any) => Event.clientX > 200))
.subscribe(console.log) //打印 mousemove 事件的 event,鼠标移动到clientX 小于 200 的时候,停止
takeUntil#
直到 notifier
Observable 发出值
const btn = document.getElementById('btn')
if (btn != null) {
fromEvent(document, 'mousemove')
.pipe(takeUntil(fromEvent(btn, 'click')))
.subscribe(console.log) //打印 mousemove 事件的event,点击 btn,停止
}
First#
行為跟 take(1) 一致
interval(1000).pipe(first()).subscribe(console.log)
skip 跳过#
range(1, 5).pipe(skip(3)).subscribe(console.log) //4,5
distinct 每个重复元素过滤#
返回 Observable,它发出由源 Observable 所发出的所有与之前的项都不相同的项。
of(1, 1, 2, 2, 2, 1, 2, 3, 4, 3, 2, 1).pipe(distinct())
.subscribe(x => console.log(x)); // 1, 2, 3, 4
實際上 distinct()
會在背地裡建立一個 Set
,當接收到元素時會先去判斷 Set 內是否有相同的值,如果有就不送出,如果沒有則存到 Set 並送出。所以記得盡量不要直接把 distinct 用在一個無限的 observable 裡,這樣很可能會讓 Set 越來越大,建議大家可以放第二個參數 flushes,或用 distinctUntilChanged
distinctUntilChanged 最后元素重复比较#
distinctUntilChanged 只會跟最後一次送出的元素比較,不會每個都比
of(1, 1, 2, 2, 2, 1, 1, 2, 3, 3, 4).pipe(
distinctUntilChanged(),
)
.subscribe(x => console.log(x)); // 1, 2, 1, 2, 3, 4
throttleTime 限流#
节流,可观察对象高频次发送数据流,限定时间内只能向订阅者发送一次数据流。
fromEvent(document, 'click')
.pipe(throttleTime(2000))
.subscribe(console.log) //鼠标不停点击浏览器内容,2秒内只执行一次
debounceTime 防抖#
防抖,高频次发送数据流,只响应最后一次。
fromEvent(document, 'click')
.pipe(debounceTime(2000))
.subscribe(console.log)
点浏览器内容N次,过2秒执行最后1次(最新1次)的结果
点击1次
distinctUntilChanged 检测数据流是否和上次相同#
检测数据源发出的数据流是否和上次一样,相同就忽略,不相同就发出。
组合操作符#
forkjoin 合并多个请求,直到发出的最后一个值#
类似 Promise.all(),等待 Observables 完成,然后合并它们发出的最后一个值。
import { forkJoin, of, timer } from 'rxjs';
const observable = forkJoin({
foo: of(1, 2, 3, 4),
bar: Promise.resolve(8),
baz: timer(4000)
});
observable.subscribe({
next: value => console.log(value),
complete: () => console.log('This is how it ends!'),
});
// Logs:
// { foo: 4, bar: 8, baz: 0 } ==> after 4 seconds
// 'This is how it ends!' ==> 紧接着
startWith 使用前加塞#
例子1
interval(1000).pipe(startWith(5)).subscribe(console.log)
//5,0,1,2,3, ...
例子2
of("from source")
.pipe(startWith("first", "second"))
.subscribe(x => console.log(x));
// results:
// "first"
// "second"
// "from source"
concat 串联#
当第一个Observable完成时,concat
会自动切换到下一个Observable,并继续发射值
var obs1 = interval(1000).pipe(take(5));
var obs2 = interval(500).pipe(take(2));
var obs3 = interval(2000).pipe(take(1));
var example = concat(obs1, obs2, obs3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
concatAll 高阶 Observable 转化为一阶 Observable#
它会将Observable流中的每个值视为一个Observable,并将这些内部Observable按顺序连接起来
如果Observable流中的某个值是一个数组或其他集合,concatAll
将递归地处理这些集合中的元素。
var obs1 = interval(1000).pipe(take(5));
var obs2 = interval(500).pipe(take(2));
var obs3 = interval(2000).pipe(take(1));
var source = of(obs1, obs2, obs3);
var example = source.pipe(concatAll());
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
merge 并联#
创建一个输出 Observable ,它可以同时发出每个给定的输入 Observable 中的所有值
var obs1 = interval(1000).pipe(take(10));
var obs2 = interval(500).pipe(take(5));
merge(obs1,obs2).subscribe(console.log)
例如一個影片播放器有兩個按鈕,一個是暫停(II),另一個是結束播放(口)。這兩個按鈕都具有相同的行為就是影片會被停止,只是結束播放會讓影片回到 00 秒,這時我們就可以把這兩個按鈕的事件 merge 起來處理影片暫停這件事
var stopVideo = Rx.Observable.merge(stopButton, endButton);
stopVideo.subscribe(() => {
// 暫停播放影片
})
combineLatest 多输入操作符,组合多个observable 的最新值#
取得各個 observable 最後送出的值,再輸出成一個值
const firstTimer = timer(0, 1000); // 从现在开始,每隔1秒发出0, 1, 2...
const secondTimer = timer(500, 1000); // 0.5秒后,每隔1秒发出0, 1, 2...
const combinedTimers = combineLatest(firstTimer, secondTimer);
combinedTimers.subscribe(value => console.log(value));
// [0, 0] after 0.5s
// [1, 0] after 1s
// [1, 1] after 1.5s
// [2, 1] after 2s
combineLatest 很常用在運算多個因子的結果,例如最常見的 BMI 計算,我們身高變動時就拿上一次的體重計算新的 BMI,當體重變動時則拿上一次的身高計算 BMI,這就很適合用 combineLatest 來處理!
combineLatest产生的Observable何时完结呢?这一点上combineLatest又和zip不一样,单独某个上游Observable完结不会让combineLatest产生的Observable对象完结,因为当一个Observable对象完结之后,它依然有“最新数据”啊,就是它在完结之前产生的最后一个数据,combineLatest记着呢,还可以继续使用这个“最新数据”。只有当所有上游Observable对象都完结之后,combineLatest才会给下游一个complete信号,表示不会有任何数据更新了
withLatestFrom 单输入操作符#
每当源 Observable 发出值,它会计算一个公式,此公式使用该值加上其他输入 Observable 的最新值,然后发出公式的输出结果。
所有输入Observable的地位并不相同,调用 withLatestFrom 的那个Observable对象(例1的 source1$
)起到主导数据产生节奏的作用,作为参数的Observable对象 (例1的source2$
) 只能贡献数据,不能控制产生数据的时机。
例子1
const source1$ = timer(0, 2000).pipe(map(x => 100 * x), tap( x=> console.log(`source1:${x}`)));
const source2$ = timer(500, 1000).pipe(tap( x=> console.log(`source2:${x}`)));
const result$ = source1$.pipe(withLatestFrom(source2$, (a, b)=> a+b));
result$.subscribe(
console.log,
null,
() => console.log('complete')
);
在输出中,所有输出的百位数由source1$
贡献,个位数由source2$
贡献。可以看到,source2$
虽然产生的是连续递增的整数序列,但并不是所有数据都进入了最终结果,很明显2
和4
就没有出现在最终结果的个位;而source1$
产生的数据,除了第一个0
,其余全部出现在了最终结果的百位。
例子2
var clicks = fromEvent(document, 'click');
var timer = interval(1000);
var result = clicks.pipe(withLatestFrom(timer));
result.subscribe(x => console.log(x));
// 22:17:59.042 [PointerEvent, 0]
// 22:18:01.746 [PointerEvent, 3]
// 22:18:04.355 [PointerEvent, 5]
// 22:18:06.859 [PointerEvent, 8]
combineLatest
是一个多输入操作符,它接收多个流作为输入,然后当任何一个输入流发出值时,都会触发结果流发出一个新的值。结果流发出的值是根据每个输入流发出的最新值计算得到的。这个操作符可以用于当多个流中的任何一个发生变化时,都需要对最新的值进行一些处理的情况。
withLatestFrom
则是一个单输入操作符,它接收一个流作为输入,然后当这个流发出值时,才会触发结果流发出一个新的值。但是,这个新的值并不是根据输入流发出的最新值计算得到的,而是根据输入流和另一个流的最新值计算得到的。这个操作符可以用于当主事件流发生变化时,需要获取另一个流的最新值进行一些处理的情况。因此,
combineLatest
和withLatestFrom
的主要区别在于它们的行为和用途上。combineLatest
用于当多个流中的任何一个发生变化时都需要处理的情况,而withLatestFrom
则用于当主事件流发生变化时需要获取另一个流的最新值进行处理的情况。一般来说,当要合并多个Observable的“最新数据”,要从
combineLatest
和withLatestFrom
中选一个操作符来操作,根据下面的原则来选择:❑ 如果要合并完全独立的Observable对象,使用combineLatest。
❑ 如何要把一个Observable对象“映射”成新的数据流,同时要从其他Observable对象获取“最新数据”,就是用withLatestFrom。
withLatestFrom 很常用在一些 checkbox 型的功能,例如說一個編輯器,我們開啟粗體後,打出來的字就都要變粗體,粗體就像是 some observable,而我們打字就是 main observable。
zip:拉链式组合#
一对一组合
const source1$ = of(1, 2, 3);
const source2$ = of('a', 'b', 'c');
const zipped$ = zip(source1$, source2$);
zipped$.subscribe(
console.log,
null,
() => console.log('complete')
);
输出:
[ 1, 'a' ]
[ 2, 'b' ]
[ 3, 'c' ]
complete
注意:
下面这个例子,source1
是由 interval
产生的数据流,是不会完结的,但是zip产生的Observable对象却在 source2
吐完所有数据之后也调用了complete,也就是说,只要任何一个上游的Observable完结。zip只要给这个完结的Observable对象吐出的所有数据找到配对的数据,那么zip就会给下游一个complete信号
const source1$ = interval(1000);
// const source1$ = of(1, 2, 3);
const source2$ = of('a', 'b', 'c');
const zipped$ = zip(source1$, source2$);
zipped$.subscribe(
console.log,
null,
() => console.log('complete')
);
输出:
[ 0, 'a' ]
[ 1, 'b' ]
[ 2, 'c' ]
complete
race 胜者通吃#
多个Observable对象在一起,看谁最先产生数据
例子1
const source1$ = timer(0, 2000).pipe(map(x => x+'a'));
const source2$ = timer(500, 1000).pipe(map(x => x+'b'));
const winner$ = race(source1$,source2$);
winner$.subscribe(
console.log,
null,
() => console.log('complete')
);
其中source1$
以 2 秒钟的间隔产生包含 a 的字符串,source2$
以 1 秒种的间隔产生包含 b 的字符串,从产生数据的频率上看,source2$
似乎更“快”一些,不过,race看的可不是产生数据的频率快慢,而是看哪一个Observable对象最先产生第一个数据。虽然source2$
产生数据的频率快,但是它产生第一个数据要比source1$
晚500毫秒,就因为这500毫秒,source2$
失去了先机,race就会退订source2$
,完全从source1$
中拿数据。
例子2
const obs1 = interval(1000).pipe(mapTo('fast one'));
const obs2 = interval(3000).pipe(mapTo('medium one'));
const obs3 = interval(5000).pipe(mapTo('slow one'));
race(obs3, obs1, obs2).subscribe((winner) => console.log(winner));
高阶 Observable#
const ho$ = interval(1000).pipe(
take(2),
map(x => interval(1500).pipe(
map(y => x+':'+y),
take(2)
))
);
上面的代码产生了一个高阶Observable对象ho$
(ho就是Higher Order的缩写),首先通过interval
和take
产生间隔 1 秒钟的两个数据 0 和 1,但是ho$
要的不是这两个数据,因为接下来又通过map
把 0 和 1 映射为新的Observable对象,这样,ho$
这个Observable对象中产生的数据依然是Observable对象,这就是高阶Observable对象。
名称就是在原有操作符名称的结尾加上All,如下所示:
❑ concatAll
❑ mergeAll
❑ zipAll
❑ combineAll(这个是个例外,因为combineLatestAll显得有点啰嗦)All代表“全部”,这些操作符的功能有差异,但都是把一个高阶Observable的所有内部Observable都组合起来,所有这类操作符全部都只有实例操作符的形式。
concatAll 内部 Observable 首尾相连#
concat是把所有输入的Observable首尾相连组合在一起,concatAll做的事情也一样,只不过concatAll只有一个上游Observable对象,这个Observable对象预期是一个高阶Observable对象,concatAll会对其中的内部Observable对象做concat的操作。
const ho$ = interval(1000).pipe(
take(2),
map(x => interval(1500).pipe(
map(y => x+':'+y),
take(2)
))
);
const concated$ = ho$.pipe(concatAll());
concated$.subscribe(
console.log,
null,
() => console.log('complete')
);
0:0
0:1
1:0
1:1
complete
可以看到差异,第二个内部Observable会产生1:0和1:1两个数据,但是1:0这个数据在第一个内部Observable的0:1之前就产生了,但在经过concatAll之后1:0出现在0:1之后,为什么?
这是因为concatAll首先会订阅上游产生的第一个内部Observable对象,抽取其中的数据,然后,只有当第一个Observable对象完结的时候,才会去订阅第二个内部Observable对象。也就是说,虽然高阶Observable对象已经产生了第二个Observable对象,不代表concatAll会立刻去订阅它,因为这个Observable对象是懒执行,所以不去订阅自然也不会产生数据,最后生成1:0的时间也就被推迟到产生0:1之后。
和concat一样,如果前一个内部Observable没有完结,那么concatAll就不会订阅下一个内部Observable对象,这导致一个问题,如果上游的高阶Observable对象持续不断产生Observable对象,但是这些Observable对象又异步产生数据,以至于concatAll合并的速度赶不上上游产生新的Observable对象的速度,这就会造成Observable的积压。在上面的例子中,高阶Observable对象每隔1秒钟产生一个内部Observable,而每个内部Observable对象产生完整数据要3秒钟时间,所以concatAll消耗内部Observable的速度永远追不上产生内部Observable对象的速度。如果无限产生这样的内部Observable,就会造成数据积压,注意,这里积压的数据并不是0:0、1:0这样的数据,而是内部Observable对象的积压,最终这样的积压就是内存泄露。如何处理这样的数据积压问题,使用
switch
和exhaust
。
switch 切换 (v7删除)#
const ho$ = interval(1000).pipe(
take(3),
map(x => interval(700).pipe(
map(y => x+':'+y),
take(2)
))
);
const concated$ = ho$.pipe(switch());
concated$.subscribe(
console.log,
null,
() => console.log('complete')
);
exhaust 耗尽 (v8删除)#
重命名为 exhaustAll
。将在 v8 中删除。
const ho$ = interval(1000).pipe(
take(3),
map(x => interval(700).pipe(
map(y => x+':'+y),
take(2)
))
);
const concated$ = ho$.pipe(exhaust());
concated$.subscribe(
console.log,
null,
() => console.log('complete')
);
辅助类操作符#
数学和聚合操作符#
count:统计数据个数#
例子1
const source$ = concat(of(1, 2, 3),of(4, 5, 6));
const count$ = source$.pipe(
count()
);
count$.subscribe(console.log) //结果:6
例子2
const source$ = concat(timer(1000),timer(1000));
const count$ = source$.pipe(
count()
);
count$.subscribe(console.log) //结果:2
max和min:最大最小值#
max和min的用法相同,唯一区别就是max是取得上游Observable吐出所有数据的“最大值”,而min是取得“最小值”
如果Observable吐出的数据类型是复杂数据类型,比如一个对象,那必须指定一个比较这种复杂类型大小的方法,所以,max和min这两个操作符都可以接受一个比较函数作为参数。
const intialReleaseS$ = of(
{name: 'RxJS', year: 2011},
{name: 'React', year: 2013},
{name: 'Redux', year: 2015}
);
const min$ = intialReleaseS$.pipe(min((a, b) => a.year - b.year));
min$.subscribe(console.log) //{name: 'RxJS', year: 2011}
reduce:规约统计#
function(accumulation, current) {
//accumulation是当前累计值,current是当前数据
//函数应该返回最新的累计值
}
除了规约函数,reduce还有一个可选参数seed,这是规约过程中“累计”的初始值,如果不指定seed参数,那么数据集合中的第一个数据就充当初始值,当然,这样第一个数据不会作为current参数调用规约函数,而是直接作为accumulation参数传递给规约函数的第一次调用
const source$ = range(1, 100)
const reduced$ = source$.pipe(reduce(
(acc, current) => acc + current,
0
));
其中,reduce有两个参数,第一个参数是规约函数,第二个参数是“累积”值的初始值,也叫“种子值”,这里种子值当然为0。规约函数的第一个参数acc代表当前的“累积”,第二个参数current是当前吐出的元素值。
条件布尔类操作符#
every 全部满足#
每个条目全都满足指定的条件
of(1, 2, 3, 4, 5, 6)
.pipe(every(x => x < 5))
.subscribe(x => console.log(x)); // -> false
find 和 findIndex 查找第1个数据(位置)#
满足判定条件的第一个数据,产生的Observable对象在吐出数据之后会立刻完结
两者不同之处是,find会吐出找到的上游数据,而findIndex会吐出满足判定条件的数据序号
find
const source$ = of(3, 1, 4, 1, 5, 9);
const find$ = source$.pipe(find(x => x % 2 === 0));
find$.subscribe(x => console.log(x)); //4
findIndex
const source$ = of(3, 1, 4, 1, 5, 9);
const find$ = source$.pipe(findIndex(x => x % 2 === 0));
find$.subscribe(x => console.log(x)); //2 (这里指位置) ==> 数据:4
const div = document.createElement('div');
div.style.cssText = 'width: 200px; height: 200px; background: #09c;';
document.body.appendChild(div);
const clicks = fromEvent(document, 'click');
const result = clicks.pipe(find(ev => (<HTMLElement>ev.target).tagName === 'DIV'));
result.subscribe(x => console.log(x));
isEmpty 检查为空#
isEmpty用于检查一个上游Observable对象是不是“空的”,所谓“空的”Observable是指没有吐出任何数据就完结的Observable对象
例子:为空的 Observable 发出 true
const result = EMPTY.pipe(isEmpty());
result.subscribe(x => console.log(x));
// Outputs
// true
例子:为非空 Observable 发出 false
const source = new Subject<string>();
const result = source.pipe(isEmpty());
source.subscribe(x => console.log(`source:Subject的订阅${x}`));
result.subscribe(x => console.log(`result:isEmpty的订阅${x}`));
source.next('a');
source.next('b');
source.next('c');
source.complete();
// source:Subject的订阅a
// result:isEmpty的订阅false
// source:Subject的订阅b
// source:Subject的订阅cmplete();
defaultIfEmpty 检查为空,为空则赋个默认值#
defaultIfEmpty
会发送由源 Observable 发送的值,或者如果源 Observable 为空(在还没有发送任何 next
值时就已完成)则发送指定的默认值。
例子:如果在 5 秒内没有点击,则发送 'no clicks'
const clicks = fromEvent(document, 'click');
const clicksBeforeFive = clicks.pipe(takeUntil(interval(5000)));
const result = clicksBeforeFive.pipe(defaultIfEmpty('no clicks'));
result.subscribe(x => console.log(x));
Rxjs 代码演示#
https://rxviz.com/examples/basic-interval
Rxjs 实时弹珠图#
作者:【唐】三三
出处:https://www.cnblogs.com/tangge/p/17673085.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
2013-09-01 C#回顾 - 3.NET的IO:字节流