RxJS 从入门到原理

1,597 阅读9分钟

作者:Today吃撑了吗

文章作者授权本账号发布,未经允许请勿转载

RxJS

ReactiveX

An API for asynchronous programming with observable streams

lodash 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

Callback Hell

使用 Promise 可以很好的解决这个问题,使用链式调用代码结构扁平,逻辑清晰

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方法中定义了一个观察者,里面有三个方法:nexterrorcomplete,这三个方法共同组成了一个观察者。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:

  1. 进行转换:map, filter, reduce
  2. 可以互相组合:concat, merge, zip
  3. 在时间维度上处理事件流:debounce, throttle, buffer, combineLatest
  4. Lazy:retry, repeat

了解更多Operators的用法

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 的最新值。

combineLatest

在函数式组件中使用 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 来代替操作符的链式调用

RxJS Before and After

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

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函数中:

  • 参数inputreduce函数的初始值,在这里为调用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 的异步事件

redux-observable

Redux + RxJS = Amazing!

2. 这是一个新的轮子吗?

Rx是已经很成熟的库,如果以前没有接触过Rx,不如说这是一种新的编程思想,它在众多语言中有对应的实现。苹果在 WWDC19 上发布了自己的 Reactive 框架 Combain,将响应式引入到了 SwiftUI 中,在这里可以查看它们之间的一些差异,

RxSwift to Apple’s Combine “Cheat Sheet”

RxSwift to Combain

参考链接

  1. ReactiveX
  2. 观察者模式与订阅发布模式的区别
  3. Combine vs RxSwift: Introduction to Combine and Differences
  4. ReactiveX 使用介绍
  5. reactive.how
  6. 响应式编程入门指南 - 通俗易懂 RxJS
  7. React and Rx.js - The Power Of Observable