響應式編程實戰—— RxJS 改變事件流與合并事件流

今天我們來優化一下之前的程序。在 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>
  );
}

有任何問題,請添加微信公眾號“讀一讀我”。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373