RxJS 之于异步,就像 JQuery 之于 dom

413次阅读  |  发布于2年以前

记得当年我刚学 JavaScript 的时候,是从原生的 dom api 学起的,用原生的 dom api 完成一些增删改的功能,之后就会学习 JQuery。

刚接触 JQuery 的时候,感觉这也太爽了吧。比如这样一段逻辑:

创建一个 p 标签包含一个文本节点,然后插入到 container 中。

用原生 dom api 写是这样的:

const containerDom = document.getElementById('container');

const textDom = document.createTextNode("Hello World.");
const paraDom = document.createElement("p");
paraDom.appendChild(textDom);

containerDom.appendChild(paraDom);

而用 JQuery 写是这样的:

const $container = $('#container');

$container.append('<p>Hello World.</p>');

比起用原生 dom api 来写简化太多了!这也是为什么 JQuery 当年那么火的原因。

虽然现在都用 Vue、React 这种数据驱动的前端框架来写页面,基本不直接操作 dom 了。但涉及到一些活动页等需要直接操作 dom 的场景,用 JQuery 依然很方便。

那 JQuery 做了什么呢?

JQuery 把 dom 封装了一层,提供了很多操作 dom 的 api,并且支持链式调用,可以方便的组织 dom 操作逻辑,而且还支持插件来自定义一些方法在链式调用中使用。

可能你会说,JQuery 不是基本用不到了么,提它干什么?

因为我觉得 JQuery 对 dom 操作的这层封装很好,把操作 dom 的复杂度降低了很多。前端除了经常操作 dom 外,还会经常处理异步,比如 XHR 和 Fetch、Event Listener 等,虽然可以用 Promise 封装,还可以进一步简化成 async/await 的写法,但是 Promise 和 async/await 只是改变了异步逻辑的书写形式,并没有降低异步逻辑编写的复杂度。 能不能就像 JQuery 对 dom 操作的封装那样,把异步逻辑也给封装一层,简化下异步逻辑的编写呢?

确实有这样的一个库,就是 Rx.js。

写 JQuery 的时候我们是把 dom 封装了一层,比如 const $container = $(dom),这样就能用 JQuery 内置的工具函数或者通过插件扩展的一些函数,通过链式调用把逻辑串起来。

为了和 dom 对象容易区分,我们会把 JQuery 对象命名成 $、$yy 等。

那么 Rx.js 第一步要做的也是把异步逻辑包一层:

也就是把 Event Listener、Promise、回调函数这种异步代码包一层:

// 包一层 Event Listener
const observable$ = Rx.Observable.fromEvent(document.querySelector('button'), 'click'); 

// 包一层 Promise
const observable2$ = Rx.Observable.fromPromise(fetch('/users'));

// 包一层 callback
const observeable3$ = Rx.Observable.bindCallback(fs.exists);

包装以后的对象不叫 RxJS 对象,叫做 Observable 对象,而且为了便于区分,我们会把它命名为 xxx$、$,就像 JQuery 对象我们会命名成 $、$yyy 一样。

然后就可以用内置的一系列工具函数了,这些叫做操作符 operator:

observable$.pipe(
    throttleTime(300),
    take(3),
    map(event => event.target.value)
);

比如异步逻辑我们经常做节流处理,那就不用自己写了,直接用内置的操作符 throttleTime 就行。

还有忽略前三次事件 take(3),对数据做一次映射 map(() => xxx) 等等这些常见异步逻辑用操作符来写就很简单。

把异步逻辑组织成链条(或者叫管道 pipe),用操作符来写每步的处理逻辑,然后串联起来,这样就把异步逻辑的书写变为了 pipe 的组织。而且就像 JQuery 可以写插件来扩展一样,Rxjs 也支持自定义操作符。

经过这个管道之后,数据经过了每一步异步逻辑的处理,我们可以通过 subcribe 监听,拿到最终的数据。

observerable$.subscribe((value) => {
    // xxx
})

当然,也可能在处理的过程中出错了,那也要把 error 传下去,并且最终处理完以后也会有个通知,所以可以写这样三种情况的处理:

observerable$.subscribe({
    next: (v) => {},
    error: (err) =>{},
    complete: () => {}
});

这些处理逻辑叫做 Observer。

这就是 RxJs 做的事情了。因为异步逻辑是对某个事件的响应,这也叫做响应式编程

刚才我们创建 Observable 是包了一层 Event Listener、callback、Promise,当然也可以直接创建这样一个 Observable 对象:

比如我们把一系列数封装成 Observable:

// 多个数据
const observable$ = Rx.Observable.of(1, 2, 3); 

// 数组中的多个数据
const observable2$ = Rx.Observable.from([1,2,3]);

或者经过一些逻辑逻辑产生一系列数据:

var observable$ = new Rx.Observable(function (observer) {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    setTimeout(() => {
        observer.next(4);
        observer.complete();
    }, 1000);
});

或者这样:

const observable$ = new Rx.Subject();

observable$.next(1);
observable$.next(2);

这里的区别是 Subject 是可以在外部调用 next 来产生数据的,而 new Observable 是在回调函数内调用 next 产生数据。

我们小结一下:

就像 JQuery 对 dom 包了一层,然后提供了简化 dom 操作的 api 一样,RxJS 也对异步逻辑包装了一层,然后提供了一系列 operator。我们可以把 EventListenr、Promise、callback 等包装成 Observable(或者自己用 of、from、Subject 等创建 Observable),然后用内置的或者自己扩展的 oprator 组织处理管道,在管道的末尾用 Observer 接受数据、处理错误。这样就把异步逻辑的编写,转变为了操作符管道的组织。当对内置的 operator 足够熟练或者自己沉淀了一些 operator 之后,写异步的逻辑速度会变得很快。

因为 RxJS 只是对异步逻辑的封装,和 Vue、React 等前端框架并不冲突,所以可以很好的结合在一起。(Angular 甚至默认就集成了 RxJS)

比如在 Vue 里面,我们可以把事件用 Subject 封装成一个 Observable,然后就可以用 RxJS 的操作符来组织异步逻辑了:

<div @click="clickHandler">点我</div>
import { Subject } from 'rxjs'
import { debounceTime } from 'rxjs/operators'

export default {
   data() {
     return {
       observable$: new Subject()
     }
   },
   created() {
      this.observable$.pipe(debounceTime(500)).subscribe((event) => {
        // xxx
      })
   },
   methods: {
      clickHandler(event) {
         this.observable$.next(event)
      }
   }
}

在 React 里面也一样,用 Subject 自己创建个 Observale,就可以把异步逻辑的编写转变为 operator 的组装了:

class MyButton extends React.Component {
    constructor(props) {
       super(props);
       this.state = { count: 0 };

       this.observable$ = new Rx.Subject();

       this.observable$.pipe(
           debounceTime(500),
           map(() => 1),
           scan((total, num) => total + num, 0)
       );

       this.observable$.subscribe(x => {
          this.setState({ count: x })
       })
    }
    render() {
        return <button onClick={event => this.observable$.next(event)}>{
            this.state.count
        }</button>
    }
}

我们用 Subject 创建了个 Observable 对象,每次点击都调用 next 产生一个数据,传入处理管道。

管道我们是用 operator 组织的,先做了 500ms 的截流,然后把值变为 1,之后计数。

处理完之后传递给 Observer 的就是累加后的数值,设置到 state 即可。

这样一段节流 + 计数的异步逻辑就写完了,其实就是组装了下 operator,这就是 RxJS 的意义。

总结

用原生的 dom api 进行 dom 操作比较繁琐,所以我们会使用 JQuery,它把 dom 包了一层,提供了很多方便的内置 api,而且还支持通过插件扩展,这样极大的简化了 dom 操作。

除了操作 dom,前端开发还经常要写异步逻辑,同样也需要这样一个包一层的库来简化,它就是 Rx.js。

Rx.js 把 Event Listener、Promise、callback 等封装成了 Observable(也可以自己创建),提供了很多操作符 operator(还可以自定义),用它们来组装成处理管道(pipe)来处理异步逻辑,最后传入 Observer 来接收数据和处理错误。这样把异步逻辑的编写转变为了 operator 的组装,把填空题变为了选择题,异步逻辑的编写速度和体验自然会提升很多。

而且,RxJS 是专门处理异步逻辑的,可以和前端框架很好的结合在一起使用。

就像用 JQuery 操作 dom 很爽一样,熟悉了 RxJS 的 operator,用 RxJS 编写(组装)异步逻辑的体验也非常棒。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8