☘️RxJs操作符

RxJS learning

mergeMap

RxJS最佳实践

来自官网~

Observable对象可以简化输入提示建议的实现方法,典型的输入提示完成一系列的独立的任务。

  1. 从输入中监听数据。
  2. 移除输入值前后的空白字符,并确认它达到了最小长度。
  3. 防抖(这样才能防止连续按键时每次都发起API请求,而应该等到按键出现停顿时才发起)。
  4. 如果输入的值没有发生变化,则不要发起请求(比如按下某个字符,然后快速按退格)。
  5. 如果已发出的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)))
)

RxFib

分类整理

创建操作符

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<T>> 二维 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();

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270