作者:Today吃撑了吗
文章作者授权本账号发布,未经允许请勿转载
ReactiveX
An API for asynchronous programming with observable streamslodash for async
异步事件
RxJS 是处理异步事件的工具库,关于异步事件,我们可以思考一下来源于哪里?
常见的异步事件(Async Events)
- 用户交互(鼠标、键盘、页面滚动...)
- 网络请求(Http、WebSocket)
- 计时器(setTimeout、setInterval)
- 动画(transitionend、animationend...)
- 其他(Workers、iframe...)
在 React 中一般是这样处理事件的:
handleClick = (event) => {
// To do something ...
this.setState(...)
}
<Button onClick={handleClick}>Click</Button>
在某些些场景下需要对异步事件进行控制,比如:
- 对事件进行防抖、节流、缓存...
- AJAX 请求的取消与组合
- 对拖拽事件处理(Drag and Drop)
- 处理WebSocket、Workers、iframe 通信等
处理异步事件的方式
最常见的方法是使用回调函数 Callback,在回调函数中做一些判断逻辑
fetchSomeData((error, data) => {
if(!error) {
dispatch({ type:'HERES_THE_DATA', data});
}
});
但是异步调用层数过多会陷入 Callback Hell
使用 Promise
可以很好的解决这个问题,使用链式调用代码结构扁平,逻辑清晰
Promise 特点
- 保证有结果:未来一定会得到一个执行结果(成功、失败)
- 状态不可逆:创建后立即执行,无法中途取消,状态完成后不可改变
- 只有一个值:完成后只得到一个值且只有一次,与then调用次数无关
思考一下在项目中什么情况下用到了 Promise ?
大部分情况下可能只有一个答案:发起 http 请求
因为在这些异步事件中只有 http 请求是一次性的,其他异步事件都是多次重复发生的,所以Promise只适用处理一部分类型的异步事件,我们大部分异步事件是用 Callback 处理的。
另外http请求在某些情况下是需要取消的:
- 切换路由、页面
- AutoComplete 搜索
- 用户要求取消
这些都是很常见并容易忽视的场景。
RxJS 中的一些概念
Observable(可观察对象)
为了解决Promise面临的问题,RxJS 引入了 Observable 概念
- Observable 是一个事件流:零个、一个值或多个值在时间维度上的集合
- Observable 可处理任何时间发生的事件
- Observable 可取消
从时间维度上看,就是在不同时间依次发生的事件。比如说依次点击屏幕,如下可以看做流式的事件
Observable 是一个时间维度上的事件集合。
创建一个 Observable
RxJS 提供了多种创建 Observable 的方法
import { of, from, interval, ajax, webSocket, fromEvent } from 'rxjs';
of('hello') // 从一个值来创建
from([1, 2, 3, 4]) // 从一系列值来创建
interval(1000) // 定时器,每秒发出一个事件
ajax('http://example.com') // 从ajax请求中创建
webSocket('ws://echo.websocket.com') // 从 websocket 来创建
fromEvent(button, 'click') // 从 DOM 事件中创建
// many more...
观察 Observable 的变化
通过 Observable 的subscribe
方法来观察事件流
myObservable.subscribe(
value => console.log('next', value), // 正常事件
err => console.error('error', err), // 异常事件,可选
() => console.info('complete!') // 完成事件,可选
);
在subscribe
方法中定义了一个观察者,里面有三个方法:next
、error
、complete
,这三个方法共同组成了一个观察者。next
方法用来监听正常事件流中的事件,error
方法用来处理事件流中的异常事件,complete
方法用来处理事件流完成事件,当事件流完成时意味着所有事件已经结束了,这时候事件流处于完成状态,Observable 不再发出新的事件。
对事件流进行处理
Observable 和 subscribe 之间可使用pipe对数据流进行处理,pipe中放入需要的操作符。
Operators (操作符) 采用函数式编程风格的纯函数
import { interval } from 'rxjs';
import { map, filter, scan } from 'rxjs/operators';
interval(1000)
.pipe(
filter(x => x % 2 === 0),
map(x => x + x),
scan((acc, x) => acc + x)
)
.subscribe(x => console.log(x))
Operators 可以将 Observable:
- 进行转换:map, filter, reduce
- 可以互相组合:concat, merge, zip
- 在时间维度上处理事件流:debounce, throttle, buffer, combineLatest
- Lazy:retry, repeat
Subject(主题)
在实际使用过程中我们往往需要自定义实现何时需要发出一个事件,这时候就需要一个可以手动触发事件的 Observable。
Subject 本身是一个 Observable,它暴露了next
方法供外部调用来产生一个事件,同一类型的消息通过一个主题来发送
Subject 相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。
import { Subject } from 'rxjs';
const someSubject = new Subject();
someSubject.subscribe(value=>{
console.log(value);
});
someSubject.next('hello');
// output 'hello'
思考:异步事件发生后改变了什么?
使用 RxJS 进行状态管理
所有状态都是伴随着异步事件发生而改变的,对异步事件的处理最终都是为了改变状态
在 RxJS 中可以使用 BehaviorSubject,它有一个“当前值”的概念。它保存了发送给观察者的最新值。并且当有新的观察者订阅时,会立即从 BehaviorSubject 那接收到“当前值”。当一个 BehaviorSubject 被更新时,每个被订阅的组件都会用新的值来重新渲染。
使用方式
1. 组件间事件传递
import { BehaviorSubject } from 'rxjs';
// 控制显示、隐藏的 BehaviorSubject,参数为初始状态
const visibleSubject$ = new BehaviorSubject(false);
function Show(){
return <button onClick={()=>visibleSubject$.next(true)}>show</button>;
}
function Hide(){
return <button onClick={()=>visibleSubject$.next(false)}>hide</button>;
}
function Tips(){
const [visible, setVisible] = useState(false);
useEffect(() => {
// 订阅状态变化
const subscription = visibleSubject$.subscribe(setVisible);
// 组件销毁时取消订阅
return () => subscription.unsubscribe();
}, []);
return <div> {visible && 'Tips'} </div>;
}
2. Hooks
把通用的部分抽离出来自定义一个hooks 示例链接
function useObservable(observable$){
const [state, setState] = useState();
useEffect(() => {
const subscription = observable$.subscribe(setState);
return () => subscription.unsubscribe();
}, []);
return state;
}
Tips 组件可以更简单:
function Tips(){
const visible = useObservable(visibleSubject$);
return <div> {visible && 'Tips'} </div>;
}
3. 搜索、筛选、表格联动
这是一个非常常见的场景,在这里我们可以把搜索框和筛选项看做两个不同的事件源,两个事件共同作用使表格状态发生改变,这里有些细节需要考虑:
- 任意值变化时需要发起 http 请求
- 输入框需要防抖
- 发起下一次请求前取消未完成的请求
- 设置 Loading 状态
combineLatest:将多个 Observables 合并为一个 Observable ,新 Observable 的值包含每个输入 Observable 的最新值。
在函数式组件中使用 Subject 时要借助一个 Hook,如果在 Class 组件中可以直接使用 Subject
function useSubject(fn, initalValue){
const subject$ = useRef(new BehaviorSubject(initalValue)).current;
const observable$ = useRef(fn?fn(subject$):subject$).current;
const onNext = useCallback((value) => subject$.next(value), [fn]);
return [onNext, observable$];
}
完整代码如下
function SearchList(){
const [laoding, setLoading] = useState(false);
// 搜索事件
const [onSearch, search$] = useSubject((event$)=>event$.pipe(
map(event=>event?.target?.value), // 取event中的值
),'');
// 筛选事件
const [onSelect, select$] = useSubject(null, 1);
// 使用 combineLatest 将两个事件合并为一个事件流
const data = useObservable(()=>combineLatest([search$,select$]).pipe(
debounceTime(300), // 防抖
tap(()=>setLoading(true)), // 设置loading状态
// 发起一个请求
switchMap(([text,times])=>from(getSearchResult(text, times)).pipe(
catchError(error=>of([]))
)),
tap(()=>setLoading(false)),
),[]);
return <div>
<Input onChange={onSearch}/>
<Select onSelect={onSelect}>
<Select.Option value={1}>1</Select.Option>
<Select.Option value={2}>2</Select.Option>
<Select.Option value={3}>3</Select.Option>
</Select>
<Spin spinning={laoding}>
<List
bordered
dataSource={data}
renderItem={item => <List.Item>{item}</List.Item>}
/>
</Spin>
</div>;
}
Tips:也可以使用封装好的 rxjs-hooks
RxJS pipe原理
在v6.0版本之后,为了便于 Tree Shaking,rxjs 开始使用 pipe 来代替操作符的链式调用
import { interval } from 'rxjs';
import { map, filter, scan } from 'rxjs/operators';
interval(1000)
.pipe(
filter(x => x % 2 === 0),
map(x => x + x),
scan((acc, x) => acc + x)
)
.subscribe(x => console.log(x))
pipe是Observable的一个方法,在源码中是这样定义的:
class Observable<T> implements Subscribable<T> {
...
pipe(...operations: OperatorFunction<any, any>[]): Observable<any> {
return operations.length ? pipeFromArray(operations)(this) : this;
}
...
}
pipe
方法将参数列表作为操作符的集合,然后返回一个 Observable,所以一个Observable经pipe
处理后仍然可以调用pipe
函数。
pipe
参数列表为空时返回当前对象,不为空时执行 pipeFromArray
函数,pipeFromArray
函数长这样:
function pipeFromArray<T, R>(fns: Array<UnaryFunction<T, R>>)
: UnaryFunction<T, R> {
if (fns.length === 0) { // fns 为空时返回一个不做任何改变的操作符函数
return (x => x) as UnaryFunction<any, any>;
}
if (fns.length === 1) {
return fns[0]; // 只有一个操作符时直接返回这个操作符
}
return function piped(input: T): R {
return fns.reduce((prev: any, fn: UnaryFunction<T, R>) => fn(prev),
input as any);
};
多个操作符时返回piped
函数,在piped
函数中:
- 参数
input
为reduce
函数的初始值,在这里为调用pipe
的Observable对象
pipeFromArray(operations)(this) // this 指向 Observable 对象
- operations在
reduce
函数中按照它们在数组中的顺序先后执行 - 当前operation以上一个operation的执行结果作为参数,返回
fn(prev)
执行完后的值,传递给下一个操作符
操作符中的函数在调用pipe
的时候就执行了吗?
以map
操作符为例:
export function map<T, R>(project: (value: T, index: number) => R, thisArg?: any): OperatorFunction<T, R> {
return operate((source, subscriber) => {
// 来源值的索引
let index = 0;
// 订阅事件源,把所有的事件和错误发送给下一个消费者
source.subscribe(
new OperatorSubscriber(subscriber, (value: T) => {
// value 为事件值,调用操作符中的函数,把执行结果传递给下一个消费者
subscriber.next(project.call(thisArg, value, index++));
})
);
});
}
操作符返回的是一个operate
函数,通过subscribe
将事件源和下一个消费者连接起来,但是这时候并没有执行操作符中的函数。
事件源和观察者通过pipe
建立起连接,在事件源调用观察者的next
方法时依次执行pipe
中的操作符函数,最终将事件传递给观察者。
小结:调用subscribe
时会建立管道连接,没有被订阅的pipe
没有任何作用,在事件发生时操作符才会对事件进行处理
总结
RxJS可以带来哪些好处?
- RxJS 非常适合处理复杂的用户事件和异步查询,例如处理表单联动逻辑,使用RxJS pipe,在这些情况下很容易编写逻辑清晰的代码。
- 可以使用RxJS来处理和限制用户事件,从而更新应用的状态。
- 在React中非常适合进行组件之间的通信
- 它可以轻松地创建数据流并对数据流进行操作,有很多数据流操作符和数据流的创建方式,所以有很大的发挥空间(例如Websocket)。
- 对于涉及时间概念的场景,或者当需要处理事件的历史值(而不仅仅是最新的)时,推荐使用RxJS,例如:可以代替
componentWillReceiveProps
使用RxJS会有什么问题?
- 学习曲线有些陡峭,对于新人来说上手时间比较长
- 对于一些没有复杂操作场景的项目来说,可能没有必要使用RxJS
- 操作符过多在调试方面会有些麻烦
Q&A
1. Redux、MobX 不可以吗?
Redux、MobX、RxJS都可以进行状态管理,RxJS 比它们更擅长于处理异步事件,通常异步事件的发生都会伴随着状态改变,两者并不冲突,可以在一起使用,redux-observable 用了 RxJS 处理 Redux 的异步事件
2. 这是一个新的轮子吗?
Rx是已经很成熟的库,如果以前没有接触过Rx,不如说这是一种新的编程思想,它在众多语言中有对应的实现。苹果在 WWDC19 上发布了自己的 Reactive 框架 Combain,将响应式引入到了 SwiftUI 中,在这里可以查看它们之间的一些差异,
RxSwift to Apple’s Combine “Cheat Sheet”