RxJS 之于異步,就像 JQuery 之于Dom
記得當(dāng)年我剛學(xué) JavaScript 的時(shí)候,是從原生的 dom api 學(xué)起的,用原生的 dom api 完成一些增刪改的功能,之后就會學(xué)習(xí) JQuery。
剛接觸 JQuery 的時(shí)候,感覺這也太爽了吧。比如這樣一段邏輯:
創(chuàng)建一個(gè) p 標(biāo)簽包含一個(gè)文本節(jié)點(diǎn),然后插入到 container 中。
用原生 dom api 寫是這樣的:
const containerDom = document.getElementById('container');
const textDom = document.createTextNode("Hello World.");
const paraDom = document.createElement("p");
pparaDom.appendChild(textDom);
containerDom.appendChild(paraDom);
而用 JQuery 寫是這樣的:
const $container = $('#container');
$container.append('<p>Hello World.</p>');
比起用原生 dom api 來寫簡化太多了!這也是為什么 JQuery 當(dāng)年那么火的原因。
雖然現(xiàn)在都用 Vue、React 這種數(shù)據(jù)驅(qū)動(dòng)的前端框架來寫頁面,基本不直接操作 dom 了。但涉及到一些活動(dòng)頁等需要直接操作 dom 的場景,用 JQuery 依然很方便。
那 JQuery 做了什么呢?
JQuery 把 dom 封裝了一層,提供了很多操作 dom 的 api,并且支持鏈?zhǔn)秸{(diào)用,可以方便的組織 dom 操作邏輯,而且還支持插件來自定義一些方法在鏈?zhǔn)秸{(diào)用中使用。
可能你會說,JQuery 不是基本用不到了么,提它干什么?
因?yàn)槲矣X得 JQuery 對 dom 操作的這層封裝很好,把操作 dom 的復(fù)雜度降低了很多。前端除了經(jīng)常操作 dom 外,還會經(jīng)常處理異步,比如 XHR 和 Fetch、Event Listener 等,雖然可以用 Promise 封裝,還可以進(jìn)一步簡化成 async/await 的寫法,但是 Promise 和 async/await 只是改變了異步邏輯的書寫形式,并沒有降低異步邏輯編寫的復(fù)雜度。 能不能就像 JQuery 對 dom 操作的封裝那樣,把異步邏輯也給封裝一層,簡化下異步邏輯的編寫呢?
確實(shí)有這樣的一個(gè)庫,就是 Rx.js。
寫 JQuery 的時(shí)候我們是把 dom 封裝了一層,比如 const $container = $(dom),這樣就能用 JQuery 內(nèi)置的工具函數(shù)或者通過插件擴(kuò)展的一些函數(shù),通過鏈?zhǔn)秸{(diào)用把邏輯串起來。
為了和 dom 對象容易區(qū)分,我們會把 JQuery 對象命名成 $、$yy 等。
那么 Rx.js 第一步要做的也是把異步邏輯包一層:
也就是把 Event Listener、Promise、回調(diào)函數(shù)這種異步代碼包一層:
// 包一層 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 對象,而且為了便于區(qū)分,我們會把它命名為 xxx$、$,就像 JQuery 對象我們會命名成 $、$yyy 一樣。
然后就可以用內(nèi)置的一系列工具函數(shù)了,這些叫做操作符 operator:
observable$.pipe(
throttleTime(300),
take(3),
map(event => event.target.value)
);
比如異步邏輯我們經(jīng)常做節(jié)流處理,那就不用自己寫了,直接用內(nèi)置的操作符 throttleTime 就行。
還有忽略前三次事件 take(3),對數(shù)據(jù)做一次映射 map(() => xxx) 等等這些常見異步邏輯用操作符來寫就很簡單。
把異步邏輯組織成鏈條(或者叫管道 pipe),用操作符來寫每步的處理邏輯,然后串聯(lián)起來,這樣就把異步邏輯的書寫變?yōu)榱?pipe 的組織。而且就像 JQuery 可以寫插件來擴(kuò)展一樣,Rxjs 也支持自定義操作符。
經(jīng)過這個(gè)管道之后,數(shù)據(jù)經(jīng)過了每一步異步邏輯的處理,我們可以通過 subcribe 監(jiān)聽,拿到最終的數(shù)據(jù)。
observerable$.subscribe((value) => {
// xxx
})
當(dāng)然,也可能在處理的過程中出錯(cuò)了,那也要把 error 傳下去,并且最終處理完以后也會有個(gè)通知,所以可以寫這樣三種情況的處理:
observerable$.subscribe({
next: (v) => {},
error: (err) =>{},
complete: () => {}
});
這些處理邏輯叫做 Observer。
這就是 RxJs 做的事情了。因?yàn)楫惒竭壿嬍菍δ硞€(gè)事件的響應(yīng),這也叫做響應(yīng)式編程。
剛才我們創(chuàng)建 Observable 是包了一層 Event Listener、callback、Promise,當(dāng)然也可以直接創(chuàng)建這樣一個(gè) Observable 對象:
比如我們把一系列數(shù)封裝成 Observable:
// 多個(gè)數(shù)據(jù)
const observable$ = Rx.Observable.of(1, 2, 3);
// 數(shù)組中的多個(gè)數(shù)據(jù)
const observable2$ = Rx.Observable.from([1,2,3]);
或者經(jīng)過一些邏輯邏輯產(chǎn)生一系列數(shù)據(jù):
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);
這里的區(qū)別是 Subject 是可以在外部調(diào)用 next 來產(chǎn)生數(shù)據(jù)的,而 new Observable 是在回調(diào)函數(shù)內(nèi)調(diào)用 next 產(chǎn)生數(shù)據(jù)。
我們小結(jié)一下:
就像 JQuery 對 dom 包了一層,然后提供了簡化 dom 操作的 api 一樣,RxJS 也對異步邏輯包裝了一層,然后提供了一系列 operator。我們可以把 EventListenr、Promise、callback 等包裝成 Observable(或者自己用 of、from、Subject 等創(chuàng)建 Observable),然后用內(nèi)置的或者自己擴(kuò)展的 oprator 組織處理管道,在管道的末尾用 Observer 接受數(shù)據(jù)、處理錯(cuò)誤。這樣就把異步邏輯的編寫,轉(zhuǎn)變?yōu)榱瞬僮鞣艿赖慕M織。當(dāng)對內(nèi)置的 operator 足夠熟練或者自己沉淀了一些 operator 之后,寫異步的邏輯速度會變得很快。
因?yàn)?RxJS 只是對異步邏輯的封裝,和 Vue、React 等前端框架并不沖突,所以可以很好的結(jié)合在一起。(Angular 甚至默認(rèn)就集成了 RxJS)
比如在 Vue 里面,我們可以把事件用 Subject 封裝成一個(gè) Observable,然后就可以用 RxJS 的操作符來組織異步邏輯了:
<div @click="clickHandler">點(diǎn)我</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 自己創(chuàng)建個(gè) Observale,就可以把異步邏輯的編寫轉(zhuǎn)變?yōu)?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 創(chuàng)建了個(gè) Observable 對象,每次點(diǎn)擊都調(diào)用 next 產(chǎn)生一個(gè)數(shù)據(jù),傳入處理管道。
管道我們是用 operator 組織的,先做了 500ms 的截流,然后把值變?yōu)?1,之后計(jì)數(shù)。
處理完之后傳遞給 Observer 的就是累加后的數(shù)值,設(shè)置到 state 即可。
這樣一段節(jié)流 + 計(jì)數(shù)的異步邏輯就寫完了,其實(shí)就是組裝了下 operator,這就是 RxJS 的意義。
總結(jié)
用原生的 dom api 進(jìn)行 dom 操作比較繁瑣,所以我們會使用 JQuery,它把 dom 包了一層,提供了很多方便的內(nèi)置 api,而且還支持通過插件擴(kuò)展,這樣極大的簡化了 dom 操作。
除了操作 dom,前端開發(fā)還經(jīng)常要寫異步邏輯,同樣也需要這樣一個(gè)包一層的庫來簡化,它就是 Rx.js。
Rx.js 把 Event Listener、Promise、callback 等封裝成了 Observable(也可以自己創(chuàng)建),提供了很多操作符 operator(還可以自定義),用它們來組裝成處理管道(pipe)來處理異步邏輯,最后傳入 Observer 來接收數(shù)據(jù)和處理錯(cuò)誤。這樣把異步邏輯的編寫轉(zhuǎn)變?yōu)榱?operator 的組裝,把填空題變?yōu)榱诉x擇題,異步邏輯的編寫速度和體驗(yàn)自然會提升很多。
而且,RxJS 是專門處理異步邏輯的,可以和前端框架很好的結(jié)合在一起使用。
就像用 JQuery 操作 dom 很爽一樣,熟悉了 RxJS 的 operator,用 RxJS 編寫(組裝)異步邏輯的體驗(yàn)很非常棒。