今天我們來優(yōu)化一下之前的程序惩猫。在 scan 中我們以匿名函數(shù)的形式對一個對象的屬性了進行了加 1 操作蛤签,我們可以把這個匿名函數(shù)變成具名函數(shù)焰望,這樣做更加靈活,復用性也更佳宦搬,對嗎锄列?因此程序變成了這樣:
const addOne = (acc) => ({count: acc.count + 1})
startBtnClick$
.pipe(
switchMapTo(intervalCanBeStopped$),
startWith({count:0}),
scan(addOne),
)
如果我們現(xiàn)在想在不改變從程序結構的情況下,點擊開始按鈕后計時器從 0 開始計數(shù)該怎么做绎橘?我們先來測試下:
const reset = (acc) => ({count: 0})
startBtnClick$
.pipe(
switchMapTo(intervalCanBeStopped$),
startWith({count:0}),
scan(reset),
)
當我們點擊開始按鈕時,會發(fā)現(xiàn)程序一直輸出 0,說明重置生效了煤辨。到這里裳涛,我們總結出傳遞給 scan 不同的行為會有不同的結果。然而有什么辦法可以不用我們手動拷貝粘貼众辨,而是通過某個操作符來完成呢端三?
mapTo:這個操作符接收一個參數(shù),將原事件流中的事件替換成這個參數(shù)鹃彻。
const addOne = (acc) => ({count: acc.count + 1})
startBtnClick$
.pipe(
switchMapTo(intervalCanBeStopped$),
mapTo(addOne),
startWith({count:0}),
scan(/*todo*/),
)
通過 mapTo 操作符郊闯,我們把時間間隔事件流中的事件都變成了 addOne 函數(shù),也就是說傳遞給 scan 的是一個函數(shù)蛛株,scan 中的累積函數(shù)該怎樣寫呢团赁?
const addOne = (acc) => ({count: acc.count + 1})
startBtnClick$
.pipe(
switchMapTo(intervalCanBeStopped$),
mapTo(addOne),
startWith({count:0}),
scan((acc, curr) => curr(acc)),
)
我們解釋一下。首先到達 scan 操作符的事件為 startWith 中的參數(shù)泳挥,也就是 {count: 0}然痊,也就是說,累積函數(shù)第一次運行的返回值為 {count: 0}屉符,這個返回值將作為下一次運行的 acc 參數(shù)剧浸。搞清楚這一點,我們再來看第二個到達 scan 操作符的事件是什么矗钟,很明顯是 addOne 函數(shù)唆香,累積函數(shù)中的 curr 參數(shù)將被賦值為這個函數(shù)。現(xiàn)在累積函數(shù)的參數(shù)都已經(jīng)確定了吨艇,返回值該怎么寫呢躬它?很明顯,把 acc 作為參數(shù)傳遞給 curr 函數(shù)东涡,計算出返回值冯吓,也就是再下一次的 acc 的值,再下一次到來的還是 addOne 函數(shù)疮跑,如果不停止组贺,將一直執(zhí)行上面的操作,也就是加 1操作祖娘。這個 scan 做的事情像不像 redux 做的事情失尖?
接下來,該把重置按鈕加上了渐苏。Rx 編程模型中最有趣的事情來了掀潮,搭積木。我們該如何把 resetBtnClick$琼富,也就是重置事件流和原來的事件流拼在一起仪吧。
我們知道加 1 操作是由時間間隔流變換而來的,重置按鈕做的是清零操作鞠眉,也就是說邑商,重置事件流至少要放在 mapTo 做轉換之前(先不考慮轉換操作)摄咆,然后 mapTo 根據(jù)到達的事件做判斷,是加 1 還是重置人断,對么?那說明朝蜘,重置流應該和時間間隔流應該屬于同一個事件流恶迈。merge 操作符恰恰就是干這個的。
merge:通過查看官方文檔谱醇,我們會發(fā)現(xiàn) merge 操作符有多個重載實現(xiàn)暇仲。我們用到的是最基本的傳給它多個事件流參數(shù)。
startBtnClick$
.pipe(
switchMapTo(merge(intervalCanBeStopped$, resetBtnClick$)),
mapTo(addOne),
startWith({ count: 0 }),
scan((acc, current) => current(acc))
)
實際運行效果肯定是不對的副渴,因為根本就沒有重置操作奈附。點擊重置按鈕進行只是加 1 操作而已。事件流合并到了一起煮剧,如何區(qū)分事件呢斥滤?很簡單:
startBtnClick$
.pipe(
switchMapTo(
merge(
intervalCanBeStopped$,
resetBtnClick$
)
),
map(v => {
if (typeof v === 'number') {
return addOne
} else {
return reset
}
}),
startWith({ count: 0 }),
scan((acc, current) => current(acc))
)
這樣做確實可以實現(xiàn)我們需要的效果,但勉盅,我們仔細想一想佑颇,事件流中的事件對我們的作用只是用來區(qū)分行為,那么我們是不是可以在原始流就把各自的事件轉換為各自的行為呢草娜?當然可以挑胸,我覺得這才是 Rx 編程模型想讓我們做的。
startBtnClick$
.pipe(
switchMapTo(
merge(
intervalCanBeStopped$.pipe(mapTo(addOne)),
resetBtnClick$.pipe(mapTo(reset))
)
),
startWith({ count: 0 }),
scan((acc, current) => current(acc))
)
在合并兩個事件流之前宰闰,分別把兩個事件流中的事件轉換為了各自代表的行為茬贵,再合并為一個我們可以稱之為行為事件流的東西。完整程序代碼如下:
import React, { useRef, useEffect } from "react";
import { fromEvent, interval, merge } from "rxjs";
import { takeUntil, switchMapTo, scan, startWith, mapTo } from "rxjs/operators";
export default function App() {
const pauseBtnRef = useRef(null);
const startBtnRef = useRef(null);
const resetBtnRef = useRef(null);
const addOne = acc => ({ count: acc.count + 1 });
const reset = acc => ({ count: 0 });
useEffect(() => {
const pauseBtnClick$ = fromEvent(pauseBtnRef.current, "click");
const startBtnClick$ = fromEvent(startBtnRef.current, "click");
const resetBtnClick$ = fromEvent(resetBtnRef.current, "click");
const perSecond$ = interval(1000);
const intervalCanBeStopped$ = perSecond$.pipe(takeUntil(pauseBtnClick$));
const addOneOrReset$ = merge(
intervalCanBeStopped$.pipe(mapTo(addOne)),
resetBtnClick$.pipe(mapTo(reset))
)
const subscription = startBtnClick$
.pipe(
switchMapTo(
addOneOrReset$
),
startWith({ count: 0 }),
scan((acc, current) => current(acc))
)
.subscribe(v => console.log(v));
return () => {
subscription.unsubscribe();
};
});
return (
<div className="App">
<button ref={startBtnRef}>開始按鈕</button>
<button ref={pauseBtnRef}>暫停按鈕</button>
<button ref={resetBtnRef}>重置按鈕</button>
</div>
);
}
有任何問題移袍,請?zhí)砑游⑿殴娞枴白x一讀我”解藻。