RxJS learning
RxJS最佳实践
来自官网~
Observable对象可以简化输入提示建议的实现方法,典型的输入提示完成一系列的独立的任务。
- 从输入中监听数据。
- 移除输入值前后的空白字符,并确认它达到了最小长度。
- 防抖(这样才能防止连续按键时每次都发起API请求,而应该等到按键出现停顿时才发起)。
- 如果输入的值没有发生变化,则不要发起请求(比如按下某个字符,然后快速按退格)。
- 如果已发出的AJAX请求的结果会因为后续的修改变得无效,那就取消它。
import { fromEvent } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
const serachBox = document.getElementById('search-box');
const typeahead$ = fromEvent(searchBox, 'input').pipe(
map((e: KeyboardEvent) => (e.target as HTMLInputElement).value),
filter(text => text.length > 2),
debounceTime(10),
distinctUntilChanged(),
switchMap(() => ajax('/api/endpoint'))
);
typeahead$.subscribe(data => {
// Handle the data from the API
})
Rx实现斐波拉契数列
const { interval } = Rx;
const { scan, pluck, groupBy } = RxOperators;
interval(1000).pipe(
scan(
({ secondLast, last }) => ({
secondLast: last,
last: last + secondLast
}),
{ secondLast: 0, last: 1 }
),
pluck("secondLast"),
groupBy(n => Math.floor(Math.log10(n)))
)
分类整理
创建操作符
from、of
of(1,2,3)
from([1,2,3]) // 记得任何可列举的参数都可以用喔,也就是说像 Set, WeakSet, Iterator 等都可以当作参数!
// 传入Promise
var source = from(new Promise(resolve, reject) => {
setTimeout(()=>{
resolve('Hello RxJS!')
})
})
source.subscribe({
next: (value) => {
console.log(value);
},
error: (err) => {
console.log(error);
},
complete: () => {
console.log('complete');
}
})
// Hello RxJS!
// complete!
如果我们传入 Promise 物件实例,当正常回传时,就会被送到 next,并立即送出完成通知,如果有错误则会送到 error。
interval
repeat
这个可以在建立轮询时使用,让我们不断地发 request 来更新画面。
range
timer
当 timer 有两个参数时,第一个参数代表要发出第一个值的等待时间(ms),第二个参数代表第一次之后发送值的间隔时间,所以上面这段程式码会先等一秒送出 0 之后每五秒送出 1, 2, 3, 4...。
timer 第一个参数除了可以是数值(Number)之外,也可以是日期(Date),就会等到指定的时间在发送第一个值。
timer也可以只接受一个参数,等待参数时间后结束。
const { timer } = Rx;
//const { } = RxOperators;
timer(1000);
// 0;
// complete
转换操作符
map
mapTo
scan
buffer 缓存元素
const { timer } = Rx;
const { take, buffer } = RxOperators;
const os1$ = timer(0, 1000).pipe(take(5));
const os2$ = timer(2000, 2000).pipe(take(2));
os1$.pipe(buffer(os2$))// 0 1 2 3
bufferTime
一段时间内触发
const button = document.getElementById('demo');
const click = Rx.Observable.fromEvent(button, 'click')
const example = click
.bufferTime(500)
.filter(arr => arr.length >= 2);
example.subscribe({
next: (value) => { console.log('success'); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
这裡我们只有在 500 毫秒内连点两下,才能成功印出 'success',这个功能在某些特殊的需求中非常的好用,也能用在批次处理来降低 request 传送的次数!
bufferCount
concatMap —— 主流缓存,从流结束,立即响应
Observable
二维 Observable
const { timer } = Rx;
const { take, concatMap } = RxOperators;
const os1$ = timer(0,5000)
os1$.pipe(concatMap(() => timer(0,1000).pipe(take(5)), (s1, s2)=> {
return `${s1} - ${s2}`
}))
//Time line
// 0-------------------1------------------
// 0---1---2---3---4---0---1---2---3---4--
// 0-0 0-1 0-2 0-3 0-4 1-0 1-1 1-2 1-3 1-4
从结果可以看到,用concatMap的时候,虽然在从流还没有结束的时候,主流还在发射数据,主流会先把发射的数据缓存起来,等从流结束后立即响应主流的数据从而引发新一轮的从流发射,这有些类似与js的消息队列机制。所以我们看到它的输出流响应是连续的。
switch 处理最新
var click = Rx.Observable.fromEvent(document.body, 'click');
var source = click.map(e => Rx.Observable.interval(1000));
var example = source.switch();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
switch 最重要的就是他会在新的 observable 送出后直接处理新的 observable 不管前一个 observable 是否完成,每当有新的 observable 送出就会直接把旧的 observable 退订(unsubscribe),永远只处理最新的 observable!
switchMap —— 主流切断
map 加上 switch 简化的写法
主流-从流
var mainstream = Rx.Observable.interval(500);// 主流
mainstream.switchMap((x) => Rx.Observable.interval(200).take(5));// 从流
用switchMap的时候,从流每次只能发射2个数据0-1,
这是因为主流每发射一次触发了从流的发射,
但是在从流发射的过程中,如果主流又一次发射了数据,
switchMap会截断上一次的从流,响应本次的主流,
从而开启新的一段的从流发射。
过滤操作符
filter
first
last
skip
略过前几个送出元素
var source = Rx.Observable.interval(1000);
var example = source.skip(3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 3
// 4
// 5...
take
顾名思义就是取前几个元素后就结束.
takeUtil
takeUntil 很常使用到,他可以在某件事情发生时,让一个 observable 直送出 完成(complete)讯息.
var source = Rx.Observable.interval(1000);
var click = Rx.Observable.fromEvent(document.body, 'click');
var example = source.takeUntil(click);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// complete (点击body了
takeLast
倒过来取最后几个
var source = Rx.Observable.interval(1000).take(6);
var example = source.takeLast(2);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 4
// 5
// complete
debounceTime
throttle
注意:一开始会执行一次,等到有元素被送出就会沈默一段时间,等到时间过了又会开放发送元素。
throttleTime
distinct
var source = Rx.Observable.from([{ value: 'a'}, { value: 'b' }, { value: 'c' }, { value: 'a' }, { value: 'c' }])
.zip(Rx.Observable.interval(300), (x, y) => x);
var example = source.distinct((x) => {
return x.value
});
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// {value: "a"}
// {value: "b"}
// {value: "c"}
// complete
distinct 可以传入第二个参数 flushes observable 用来清除暂存的资料,其实 flushes observable 就是在送出元素时,会把 distinct 的暂存清空,所以之后的暂存就会从头来过,这样就不用担心暂存的 Set 越来愈大的问题
distinctUntilChanged
distinctUntilChanged 跟 distinct 一样会把相同的元素过滤掉,但 distinctUntilChanged 只会跟最后一次送出的元素比较
var source = Rx.Observable.from(['a', 'b', 'c', 'c', 'b'])
.zip(Rx.Observable.interval(300), (x, y) => x);
var example = source.distinctUntilChanged()
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// a
// b
// c
// b
// complete
这裡 distinctUntilChanged 只会暂存一个元素,并在收到元素时跟暂存的元素比对,如果一样就不送出,如果不一样就把暂存的元素换成刚接收到的新元素并送出。
结合操作符
concat
可以把多个 observable 实例合併成一个
var source = Rx.Observable.interval(1000).take(3);
var source2 = Rx.Observable.of(3)
var source3 = Rx.Observable.of(4,5,6)
var example = source.concat(source2, source3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// complete
concatAll 执行完上一个,才会继续执行
concatAll 最重要的重点就是他会处理完前一个 observable 才会在处理下一个 observable
当我们用 concatAll 之后会把二维的 observable 摊平成一维的 observable,但 concatAll 会一个一个处理,一定是等前一个 observable 完成(complete)才会处理下一个 observable。
实例中,因为现在送出 observable 是无限的永远不会完成(complete),就导致他永远不会处理第二个送出的 observable!
var click = Rx.Observable.fromEvent(document.body, 'click');
var source = click.map(e => Rx.Observable.interval(1000));
var example = source.concatAll();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// (点击后)
// 0
// 1
// 2
// 3
// 4
// 5 ...
click : ---------c-c------------------c--..
map(e => Rx.Observable.interval(1000))
source : ---------o-o------------------o--..
\ \
\ ----0----1----2----3----4--...
----0----1----2----3----4--...
concatAll()
example: ----------------0----1----2----3----4--..
startWith
可以在 observable 的一开始塞要发送的元素
var source = Rx.Observable.interval(1000);
var example = source.startWith(0);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 0
// 1
// 2
// 3...
merge
merge 把多个 observable 同时处理
merge 之后的 example 在时间序上同时在跑 source 与 source2,当两件事情同时发生时,会同步送出资料(被 merge 的在后面),当两个 observable 都结束时才会真的结束。
source : ----0----1----2|
source2: --0--1--2--3--4--5|
merge()
example: --0-01--21-3--(24)--5|
mergeAll 并行处理
var click = Rx.Observable.fromEvent(document.body, 'click');
var source = click.map(e => Rx.Observable.interval(1000));
var example = source.mergeAll();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
click : ---------c-c------------------c--..
map(e => Rx.Observable.interval(1000))
source : ---------o-o------------------o--..
\ \ \----0----1--...
\ ----0----1----2----3----4--...
----0----1----2----3----4--...
switch()
example: ----------------00---11---22---33---(04)4--...
mergeAll 可以传入一个数值,这个数值代表他可以同时处理的 observable 数量,如果我们传入 1 其行为就会跟 concatAll 是一模一样的,这点在原始码可以看到他们是完全相同的。
mergeMap —— 实时响应,永不遗漏
map 加上 mergeAll
const { timer, of } = Rx;
const { take, mergeMap, mapTo } = RxOperators;
const source$ = timer(0, 2000).pipe(mapTo('hello'),take(5))
source$.pipe(mergeMap(val => of(`${val} - world`)))
flatMap —— 实时响应,永不遗漏
// JavaScript
var mainstream= Rx.Observable.interval(500);
mainstream.flatMap((x) => Rx.Observable.interval(200).take(5));
从结果可以看出来,flatMap/mergeMap会实时的响应主流中发射的每一个数据
,它既不会忽略也不会缓存,这就导致主流中数据对应的从流产生了叠加。
exhaustMap —— 主流等待,缓缓执行
var mainstream= Rx.Observable.interval(500);
mainstream.exhaustMap((x) => Rx.Observable.interval(200).take(5));
从结果可以看出,exhaustMap在从流还没有结束的时候如果主流仍然有数据在发射,
它会忽略此时主流发射的数据,而在从流结束以后才会去响应主流中发射的数据。
combineLatest
combineLatest 很常用在运算多个因子的结果,例如最常见的 BMI 计算,我们身高变动时就拿上一次的体重计算新的 BMI,当体重变动时则拿上一次的身高计算 BMI,这就很适合用 combineLatest 来处理!
learning combineLastest
zip
zip 会把各个 observable 相同顺位送出的值传入 callback,这很常拿来做 demo 使用,比如我们想要间隔 100ms 送出 'h', 'e', 'l', 'l', 'o',
var source = Rx.Observable.from('hello');
var source2 = Rx.Observable.interval(100);
var example = source.zip(source2, (x, y) => x);
source : (hello)|
source2: -0-1-2-3-4-...
zip(source2, (x, y) => x)
example: -h-e-l-l-o|
withLatestFrom
withLatestFrom 会在 main 送出值的时候执行 callback, 但请注意如果 main 送出值时 some 之前没有送出过任何值 callback 仍然不会执行!
var main = Rx.Observable.from('hello').zip(Rx.Observable.interval(500), (x, y) => x);
var some = Rx.Observable.from([0,1,0,0,0,1]).zip(Rx.Observable.interval(300), (x, y) => x);
var example = main.withLatestFrom(some, (x, y) => {
return y === 1 ? x.toUpperCase() : x;
});
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
main : ----h----e----l----l----o|
some : --0--1--0--0--0--1|
withLatestFrom(some, (x, y) => y === 1 ? x.toUpperCase() : x);
example: ----h----e----l----L----O|
实用操作符
delay
UI 操作
demo
delay 可以延迟 observable 一开始发送元素的时间点
var source = Rx.Observable.interval(300).take(5);
var example = source.delay(500);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// 4
source : --0--1--2--3--4|
delay(500)
example: -------0--1--2--3--4|
delayWhen
UI 操作
delayWhen 的作用跟 delay 很像,最大的差别是 delayWhen 可以影响每个元素,而且需要传一个 callback 并回传一个 observable
var source = Rx.Observable.interval(300).take(5);
var example = source
.delayWhen(
x => Rx.Observable.empty().delay(100 * x * x)
);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
错误处理操作符
catch
catch 是很常见的非同步错误处理方法,在 RxJS 中也能够直接用 catch 来处理错误,在 RxJS 中的 catch 可以回传一个 observable 来送出新的值
var source = Rx.Observable.from(['a','b','c','d',2])
.zip(Rx.Observable.interval(500), (x,y) => x);
var example = source
.map(x => x.toUpperCase())
.catch(error => Rx.Observable.of('h'));
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
source : ----a----b----c----d----2|
map(x => x.toUpperCase())
----a----b----c----d----X|
catch(error => Rx.Observable.of('h'))
example: ----A----B----C----D----h|
遇到错误后也可以让Observable完成
catch(error => Rx.Observable.empty())
另外 catch 的 callback 能接收第二个参数,这个参数会接收当前的 observalbe,
var source = Rx.Observable.from(['a','b','c','d',2])
.zip(Rx.Observable.interval(500), (x,y) => x);
var example = source
.map(x => x.toUpperCase())
.catch((error, obs) => obs);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
这里会变成一个死循环
retry
通常这种无限的 retry 会放在即时同步的重新连接,让我们在连线断掉后,不断的尝试,同时也可以设置重连的次数
var source = Rx.Observable.from(['a','b','c','d',2])
.zip(Rx.Observable.interval(500), (x,y) => x);
var example = source
.map(x => x.toUpperCase())
.retry(1);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// a
// b
// c
// d
// a
// b
// c
// d
// Error: TypeError: x.toUpperCase is not a function
retryWhen
retryWhen 我们传入一个 callback,这个 callback 有一个参数会传入一个 observable,这个 observable 不是原本的 observable(example) 而是例外事件送出的错误所组成的一个 observable,我们可以对这个由错误所组成的 observable 做操作,等到这次的处理完成后就会重新订阅我们原本的 observable。
retryWhen 拿来做错误通知
组播操作运算符
multicast
multicast 可以用来挂载 subject 并回传一个可连结(connectable)的 observable
var source = Rx.Observable.interval(1000)
.take(3)
.multicast(new Rx.Subject());
var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}
var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}
source.subscribe(observerA); // subject.subscribe(observerA)
source.connect(); // source.subscribe(subject) start
setTimeout(() => {
source.subscribe(observerB); // subject.subscribe(observerB)
}, 1000);
注意: 要把 connect() 回传的 subscription 退订才会真正停止 observable 的执行
refCount
通常我们会希望有 observer 订阅时,就立即执行并发送元素,而不要再多执行一个方法(connect),这时我们就可以用 refCount
refCount 必须搭配 multicast 一起使用,他可以建立一个只要有订阅就会自动 connect 的 observable
var source = Rx.Observable.interval(1000)
.do(x => console.log('send: ' + x))
.multicast(new Rx.Subject())
.refCount();
var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}
var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}
var subscriptionA = source.subscribe(observerA);
// 订阅数 0 => 1
var subscriptionB;
setTimeout(() => {
subscriptionB = source.subscribe(observerB);
// 订阅数 0 => 2
}, 1000);
当 source 一被 observerA 订阅时(订阅数从 0 变成 1),就会立即执行并发送元素,我们就不需要再额外执行 connect.
同样的在退订时只要订阅数变成 0 就会自动停止发送
publish
等价于multicast(new Subject())
// Subject 的三种变形
// 1
var source = Rx.Observable.interval(1000)
.publishReplay(1)
.refCount();
// var source = Rx.Observable.interval(1000)
// .multicast(new Rx.ReplaySubject(1))
// .refCount();
// 2
var source = Rx.Observable.interval(1000)
.publishBehavior(0)
.refCount();
// var source = Rx.Observable.interval(1000)
// .multicast(new Rx.BehaviorSubject(0))
// .refCount();
// 3
var source = Rx.Observable.interval(1000)
.publishLast()
.refCount();
// var source = Rx.Observable.interval(1000)
// .multicast(new Rx.AsyncSubject(1))
// .refCount();
share
等价于 publish + refCount
var source = Rx.Observable.interval(1000)
.share();
// var source = Rx.Observable.interval(1000)
// .publish()
// .refCount();
// var source = Rx.Observable.interval(1000)
// .multicast(new Rx.Subject())
// .refCount();
参考
- 用可视化来理解switchMap, concatMap, flatMap,exhaustMap - https://segmentfault.com/a/1190000015232015?utm_source=tag-newest
- 学习RxJS操作符 - https://rxjs-cn.github.io/learn-rxjs-operators/operators/transformation/switchmap.html