今天我們來優化一下之前的程序。在 scan 中我們以匿名函數的形式對一個對象的屬性了進行了加 1 操作,我們可以把這個匿名函數變成具名函數,這樣做更加靈活,復用性也更佳,對嗎?因此程序變成了這樣:
const addOne = (acc) => ({count: acc.count + 1})
startBtnClick$
.pipe(
switchMapTo(intervalCanBeStopped$),
startWith({count:0}),
scan(addOne),
)
如果我們現在想在不改變從程序結構的情況下,點擊開始按鈕后計時器從 0 開始計數該怎么做?我們先來測試下:
const reset = (acc) => ({count: 0})
startBtnClick$
.pipe(
switchMapTo(intervalCanBeStopped$),
startWith({count:0}),
scan(reset),
)
當我們點擊開始按鈕時,會發現程序一直輸出 0,說明重置生效了。到這里,我們總結出傳遞給 scan 不同的行為會有不同的結果。然而有什么辦法可以不用我們手動拷貝粘貼,而是通過某個操作符來完成呢?
mapTo:這個操作符接收一個參數,將原事件流中的事件替換成這個參數。
const addOne = (acc) => ({count: acc.count + 1})
startBtnClick$
.pipe(
switchMapTo(intervalCanBeStopped$),
mapTo(addOne),
startWith({count:0}),
scan(/*todo*/),
)
通過 mapTo 操作符,我們把時間間隔事件流中的事件都變成了 addOne 函數,也就是說傳遞給 scan 的是一個函數,scan 中的累積函數該怎樣寫呢?
const addOne = (acc) => ({count: acc.count + 1})
startBtnClick$
.pipe(
switchMapTo(intervalCanBeStopped$),
mapTo(addOne),
startWith({count:0}),
scan((acc, curr) => curr(acc)),
)
我們解釋一下。首先到達 scan 操作符的事件為 startWith 中的參數,也就是 {count: 0},也就是說,累積函數第一次運行的返回值為 {count: 0},這個返回值將作為下一次運行的 acc 參數。搞清楚這一點,我們再來看第二個到達 scan 操作符的事件是什么,很明顯是 addOne 函數,累積函數中的 curr 參數將被賦值為這個函數?,F在累積函數的參數都已經確定了,返回值該怎么寫呢?很明顯,把 acc 作為參數傳遞給 curr 函數,計算出返回值,也就是再下一次的 acc 的值,再下一次到來的還是 addOne 函數,如果不停止,將一直執行上面的操作,也就是加 1操作。這個 scan 做的事情像不像 redux 做的事情?
接下來,該把重置按鈕加上了。Rx 編程模型中最有趣的事情來了,搭積木。我們該如何把 resetBtnClick$,也就是重置事件流和原來的事件流拼在一起。
我們知道加 1 操作是由時間間隔流變換而來的,重置按鈕做的是清零操作,也就是說,重置事件流至少要放在 mapTo 做轉換之前(先不考慮轉換操作),然后 mapTo 根據到達的事件做判斷,是加 1 還是重置,對么?那說明,重置流應該和時間間隔流應該屬于同一個事件流。merge 操作符恰恰就是干這個的。
merge:通過查看官方文檔,我們會發現 merge 操作符有多個重載實現。我們用到的是最基本的傳給它多個事件流參數。
startBtnClick$
.pipe(
switchMapTo(merge(intervalCanBeStopped$, resetBtnClick$)),
mapTo(addOne),
startWith({ count: 0 }),
scan((acc, current) => current(acc))
)
實際運行效果肯定是不對的,因為根本就沒有重置操作。點擊重置按鈕進行只是加 1 操作而已。事件流合并到了一起,如何區分事件呢?很簡單:
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))
)
這樣做確實可以實現我們需要的效果,但,我們仔細想一想,事件流中的事件對我們的作用只是用來區分行為,那么我們是不是可以在原始流就把各自的事件轉換為各自的行為呢?當然可以,我覺得這才是 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>
);
}
有任何問題,請添加微信公眾號“讀一讀我”。