RxJs使用指北

本文深入探讨RxJs,一个强大的JavaScript响应式库,适用于前端开发。文章介绍了RxJs的出现背景、特点,如纯净性、流动性、以值传递,以及其与Promise、函数和迭代器的区别。重点解析了RxJs的常用运算符,如of/from、map、concat、mergeAll、distinct、takeUntil等,并给出了实际应用示例,展示了如何在复杂交互场景和React组件间通信中使用RxJs。

一、出现背景

应响应式编程的需求,出现了 Rx 这样的专门用来处理数据响应式的库。Rx家族有很多: RxJava,RxSwift, Rx.NET等,这里主要介绍 RxJs 的使用。

RxJs 是一个相当灵活的,使用JavaScript语言的数据响应式库。使用相当的灵活,API繁多,学习成本低,用官网的话叫可以把 RxJS 当做是用来处理事件的 Lodash

二、关键词

ReactiveX、观察者模式、迭代器模式、函数式编程、数据管道。

三、RxJs特点

1.纯净性 (Purity):使用纯函数来产生值

下图中,fromEvent便是纯函数,函数的输出不随外部变量的影响

2.流动性 (Flow):RxJS 提供了一整套操作符来处理数据流动

常用的运算有 filter、delay、debounceTime、throttleTime、take、takeUntil、distinct、distinctUntilChanged

3.以值 (Values)传递:对于流经 observables 的值,你可以对其进行转换。

还是点击事件的例子,下图中的map和scan便可以对流经的数据值进行映射和数值计算

4.Observable (可观察对象)

Rx中一切都是可观察对象,数据值放在可观察对象中。对象通过next方法往数据管道里放入值,在接收端,可以订阅(subscribe)创建出来的同一个可观察对象,通过订阅事件异步地拿到数据变化的内容,并且可以处理传输异常。

下图中可以看出,subscribe被创建时,若管道里有值,他会依次遍历一下各个值(这可以用作局部缓存),并可自定义处理函数。对于宏任务(setTimeout)会延后判断。

上图中,我们也可以看到一个完整的 subscribe 订阅内部的构成。next函数接受管道传值;error接受传值异常后的错误信息;当响应式对象调用complete函数后,执行complete函数,并结束该次订阅。

一般上,subscribe函数调用时,只接受一个参数的话,默认就是next过来的数据值。

与函数、迭代器的区别

  • 函数和 Observables 都是惰性运算。如果你不调用函数,就不会执行。Observables 也是如此,如果你不“调用”它(使用 subscribe),也不会执行

  • ES2015 引入了 generator 函数和 iterators (function*),这是另外一种类型的拉取体系。调用 iterator.next() 的代码是消费者,它会从 iterator(生产者) 那“取出”多个值。

Observable 可以随着时间的推移“返回”多个值,这是函数所做不到的。

学习RxJs,首先得弄懂生产者消费者模式的主被动关系:

生产者消费者
拉取是被动的: 当被请求时产生数据。拉取是主动的: 决定何时请求数据。
推送是主动的: 按自己的节奏产生数据。推送是被动的: 对收到的数据做出反应。

RxJs是主动推送、被动接受模式。管道入口(可订阅对象,比如Subject)强行推送值,在出口处可选择地接受和过滤数据。

示例参考:cn.rx.js.org/manual/over…

四、常用运算符

1.of/from1. of/from1.of/from

将普通对象转换为rxjs可检测的响应式对象

不同点: of订阅后忠实的打印源对象,from会将源对象解构后依次打印。是不是有点像Promise.all ?

2.map2. map2.map

顾名思义,与数组的map类似,对于一个响应式对象,可以使用pipe管道指令获取其管道的内容,之后便可以使用各种运算符来处理数据啦。

3.concat和concatAll3. concat 和 concatAll3.concat和concatAll

合并Observable对象并以此返回其值

concat:首尾相连,将各个订阅值穿起来输出

例1:

// RxJS v6+
import{of, concat }from'rxjs';
concat(of(1,2,3),// subscribed after first completesof(4,5,6),// subscribed after second completesof(7,8,9)
)
// log: 1, 2, 3, 4, 5, 6, 7, 8, 9
.subscribe(console.log); 

例2:

import { concat, merge, defer, from } from 'rxjs'; 

console.log('Start')
const promiseA$ = defer(()=>from(new Promise((reslove, reject)=>{setTimeout(()=>{reslove('PromiseA')}, 1000)
})))
const promiseB$ = defer(()=>from(new Promise((reslove, reject)=>{setTimeout(()=>{reslove('PromiseB')}, 1000)
})))

// 会依次间隔一秒打印Start, PromiseA, PromiseB
concat(promiseA$, promiseB$).subscribe(x => console.log(x)); 

concatAll:是一个高阶的处理函数,顺序接受上游抛出的各个数据流作为它的数据, 若前面的数据流不能同步的完结,它会暂存后续数据流,当前数据流完成后它才会订阅后一个暂存的数据流。可以想成是把所有元素 concat 起来。其前置条件式必须传过来一组 Observable 对象。

例1:

concatAll 一般用于处理一次性或者短时间内有多个管道数据的情况。比如处理点击事件,在他前置的运算符,必有一个类似于map的映射,concatAll 可以将一个 Observable 对象映射为拍平的值来依次处理。

不使用 concatAll,只使用map时,输出结果:

例2:重写下上面 concat 的例2,输出相同的结果

import { concat, defer, from, of } from 'rxjs'; 
import { tap, concatAll } from 'rxjs/operators';

console.log('Start')

const promiseA$ = defer(() => from(new Promise((reslove, reject) => {console.log('PromiseA is been Subscribed ')setTimeout(()=>{reslove('PromiseA')}, 1000)
})))

const promiseB$ = defer(() => from(new Promise((reslove, reject) => {console.log('PromiseB is been Subscribed ')setTimeout(()=>{reslove('PromiseB')}, 1000)
})))

// 会依次间隔一秒打印Start, PromiseA, PromiseB
of(promiseA$, promiseB$).pipe(tap(console.log), concatAll()).subscribe(x => console.log(x)); 
4.mergeAll4. mergeAll4.mergeAll

mergeAll 不会像 concatAll 一样首尾相连输出值,其并行处理所有的 Observable,不保证输出顺序。

5.衍生操作符concatMap,mergeMap5. 衍生操作符 concatMap, mergeMap5.衍生操作符concatMap,mergeMap

concatMap, mergeMap 分别是 map + concatAllmap + mergeAll 的结合体。

上面的实战例子,接受一个弹窗关闭事件,判断值是否为confirmed,若是,则交给 mergeMap处理。

mergeMap是比较常用的请求数据的操作符,详细使用见官网:cn.rx.js.org/class/es6/O…

下面是他的执行原理:

类似的,concatMap 是按照顺序排列的,使用场景更加广泛。每个新的内部 Observable 与先前的内部 Observable 首尾串联组成。他解决了 concat 和 concatAll 的一个问题:后一个数据流拿不到前一个数据流抛出的数据。

注意: concatMap等效于 mergeMap 的并发参数设置 到 1

我们还是使用上面 concat 的同一个例子,不过控制一下两个promise的先后顺序,让后一个promise获取前一个promise的数据:

import { concat, defer, from } from 'rxjs'; 
import { concatAll, map, tap } from 'rxjs/operators'; 

console.log('Start')

const promiseA$ = defer(() => from(new Promise((reslove, reject)=>{setTimeout(() => {reslove('PromiseA')}, 1000)
})))

// 这是一个会返回数据流promiseB$的函数
const promiseB = data => from(new Promise((reslove, reject)=>{setTimeout(() => {reslove(`${data} then PromiseB`)}, 1000)
}))

// map会将把上游完成后的数据通过promiseB转换成promiseB$数据流
// 并传递给concatAll, concatAll将promiseB$连接下游数据流
// 这里将在两秒后打印出 PromiseA then PromiseB
promiseA$.pipe(map(promiseB),concatAll()
).subscribe(x => console.log(x)) 

我们使用 concatMap 改写:

import { concat, defer, from } from 'rxjs'; 
import { concatMap, map, tap } from 'rxjs/operators'; 

console.log('Start')

const promiseA$ = defer(() => from(new Promise((reslove, reject)=>{setTimeout(() => {reslove('PromiseA')}, 1000)
})))

// 这是一个会返回数据流promiseB$的函数
const promiseB = data => new Promise((reslove, reject)=>{setTimeout(() => {reslove(`${data} then PromiseB`)}, 1000)
})

// concatMap 可以接收一个返回Promise的函数或者是数据流
// 这里将在两秒后打印出 PromiseA then PromiseB
promiseA$.pipe(concatMap(promiseB)
).subscribe(x => console.log(x)) 

可以看到,单单 concatMap 就可以让后一个数据流接受前面数据流的数据,其本质就是 map + concatAll。

concatMap 引申:

考虑一个实际的业务场景。我们有一个页面,页面初始化的时候有个初始化接口获取数据,页面上有个下一步按钮,这个按钮触发的事件需要使用初始化接口的数据。

可以这样写:

import { concat, defer, from, fromEvent } from 'rxjs'; 
import { tap, concatMap } from 'rxjs/operators'; 

// 使用promise来模拟数据请求过程
const req$ = defer(() => from(new Promise((reslove, reject)=>{setTimeout(() => {reslove('This is init data')}, 1000)
})))

// 事件流
const button$ = () => fromEvent(document.getElementById('button'), 'click')

// 点击按钮后输出请求内容
// 这里会打印出 This is init data
req$.pipe(concatMap(button$, (data, event) => data)
).subscribe(x => console.log(x)) 

concatMap接受异步请求的数据,然后传给 button 的点击事件 Observable 对象,通过该事件 Observable 对象来输出结果。处理事件时, concatMap 第二个参数就派上用场了。

6.定时器6. 定时器6.定时器
import { interval, timer } from 'rxjs';

const test_interval = interval(1000);

const test_timer = timer(1000, 5000);

const test_timer = timer(1000);

export {test_interval, test_timer} 

interval会不停的按照间隔时间累加数字,从0开始。timer是一个定时器,只有一个参数时,在指定时间(毫秒)后输出一个0,两个参数时,比如上面的例子,指的是从第1秒开始,每隔5秒出书一个累加的值,从0开始。

7.异常处理throwError7. 异常处理 throwError7.异常处理throwError
import { throwError, interval, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

interval(1000).pipe(
mergeMap(x => x === 2? throwError('Twos are bad'): of('a', 'b', 'c')),
).subscribe(x => console.log(x), e => console.error(e)); 

上面的例子中,使用算子 throwError抛出错误,抛出后,订阅停止。

8.zipAll8. zipAll8.zipAll

下图中,zipAll 将 from 操作符解构出的流式数据又拼接回数组形式。

9.group与reduce9. group 与 reduce9.group与reduce

类似 SQL 中的 groupBy,接受一个函数,返回要group的对象key值。

解开 tap的注释后,可以看到打印的输出:

reduce用来聚合Observable中的数值。

10.distinct10. distinct10.distinct

去重算子。

使用RxJs来数组去重:

11.takeUtill11. takeUtill11.takeUtill

直到提供的 observable 发出值,它便完成

例1:

思考:是不是可以用来 防抖节流 呢

例2:直到用户点击后,开启定时器

12.takeLast12. takeLast12.takeLast

取流数据的最后一个(同理,有 take(index: number)函数来正向指定下标获取)

13.专用的防抖节流13. 专用的防抖节流13.专用的防抖节流

上面说可以用takeUtils模拟防抖节流,其实Rxjs有专用的防抖节流函数。

import { debounceTime, throttleTime } from 'rxjs';

Observable.fromEvent(document.getElementById("debounceTime"), "click").debounceTime(1000).subscribe(() => console.log("debounceTime"));Observable.fromEvent(document.getElementById("throttleTime"), "click").throttleTime(1000).subscribe(() => console.log("throttleTime")); 
14.Subject对象14. Subject 对象14.Subject对象

Subject继承自Observable类,同样具有数据流监听特性,其作为observable的一种载体,多用于组件之间传递。

可以推送(next)和被订阅(subscribe)

BehaviorSubject:每个新的订阅可以拿到管道中最新的那次数据

AsyncSubject:complete时会输出最新的管道数据,其余时间不输出

ReplaySubject:每次新订阅时,可指定获取管道中最新的N个数据

引申阅读,数百个操作符: http://reactivex.io/documentati…

五、应用场景

1.多事件界面
2.更加复杂的交互场景,尤其是需要组件间多级传递的情况
3.需要源源不断的流出数据的场景

六、与 Promise 对比

共同点:

1.处理异步

例1:

例2:

不同:

1.设计理念不同(发布订阅、观察者+pipe)
2.中途取消

在上面异步处理的例子中,可以选择取消定时器:即取消订阅

3.完善的函数工具

七、与 React 结合

示例:组件间传递消息

父组件:声明一个 subject,通过 prop 传递给 子组件

子组件:接受该 subject,然后往里边推送值。同理,父组件也可以推送,子组件来监听。如此达到 父子组件 全双工通信。

点击子组件的三个按钮,就会看到父组件的订阅触发了!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值