響應(yīng)式編程實(shí)戰(zhàn)——RxJS 重構(gòu)組合事件流

今天我們來看看如何改變事件發(fā)生的頻率。讓我們添加兩個(gè)按鈕,分別代表了二分之一秒產(chǎn)生一個(gè)事件的事件流和四分之一秒產(chǎn)生一個(gè)事件的事件流。根據(jù)昨天的文章,我們可以想到用 merge 操作符把它們和開始按鈕合并起來。

const time$ = merge(startBtnClick$, halfBtnClick$, quarterBtnClick$);

const subscription = time$
.pipe(
  switchMapTo(addOneOrReset$),
  startWith({ count: 0 }),
  scan((acc, current) => current(acc))
)

很明顯,這三個(gè)按鈕實(shí)現(xiàn)的效果是一樣的,還是每隔一秒產(chǎn)生一個(gè)事件。既然我們要修改事件產(chǎn)生的頻率,那么我們就要知道原來這個(gè)事件流是怎么定義的:

const perSecond$ = interval(1000);

我們看到是由 interval 的參數(shù)來控制產(chǎn)生事件的頻率,來修改一下代碼:

const time$ = merge(
  startBtnClick$.pipe(mapTo(1000)),
  halfBtnClick$.pipe(mapTo(500)),
  quarterBtnClick$.pipe(mapTo(250))
);

這樣就行了嗎?開玩笑,當(dāng)然不行了,這只是把三個(gè)按鈕的 click 事件產(chǎn)生的值做了修改。我們需要做的是修改傳給 interval 的參數(shù)。現(xiàn)在的代碼是這樣的:

const perSecond$ = interval(1000);
const intervalCanBeStopped$ = perSecond$.pipe(takeUntil(pauseBtnClick$));
const addOneOrReset$ = merge(
  intervalCanBeStopped$.pipe(mapTo(addOne)),
  resetBtnClick$.pipe(mapTo(reset))
);

const time$ = merge(
  startBtnClick$.pipe(mapTo(1000)),
  halfBtnClick$.pipe(mapTo(500)),
  quarterBtnClick$.pipe(mapTo(250))
);

const subscription = time$
.pipe(
  switchMapTo(addOneOrReset$),
  startWith({ count: 0 }),
  scan((acc, current) => current(acc))
)
.subscribe(v => setTxt(v.count));

首先,我們得知,三個(gè)按鈕點(diǎn)擊后的事件流轉(zhuǎn)到了 switchMapTo 操作符;其次,我們知道 addOneOrReset$ 是由 interval(1000) 組合而來的。也就是說,我們需要把流轉(zhuǎn)到 switchMapTo 的事件傳遞給 addOneOrReset$。這里要用到了 switchMapTo 的兄弟,switchMap 操作符。

switchMap:參數(shù)為函數(shù),這個(gè)函數(shù)接收事件流中的事件作為參數(shù),返回值為另一個(gè)事件流。

讓我們一步一步來修改代碼,首先把時(shí)間參數(shù)傳遞給 switchMap 的函數(shù)參數(shù):

const subscription = time$
.pipe(
  switchMap((time) => addOneOrReset$),
  startWith({ count: 0 }),
  scan((acc, current) => current(acc))
)
.subscribe(v => setTxt(v.count));

其次,我們需要把 addOneOrReset$ 拆開找到 interval 操作符,把 time 傳遞給它:

const subscription = time$
      .pipe(
        switchMap(time =>
          merge(
            interval(time).pipe(
              takeUntil(pauseBtnClick$),
              mapTo(addOne)
            ),
            resetBtnClick$.pipe(mapTo(reset))
          )
        ),
        startWith({ count: 0 }),
        scan((acc, current) => current(acc))
      )
      .subscribe(v => setTxt(v.count));

讓我們來梳理一下流程:

  1. 開始按鈕(1秒按鈕),1/2秒按鈕,1/4秒按鈕的點(diǎn)擊事件合并為一個(gè)事件流,事件流中的事件為三個(gè)值 1000,500,250(誰點(diǎn)擊就產(chǎn)生對(duì)應(yīng)的數(shù)字)。
  2. 時(shí)間數(shù)字來到了 switchMap 操作符,并作為輸入?yún)?shù)傳遞給了 switchMap 的函數(shù)參數(shù)。
  3. switchMap 的函數(shù)參數(shù)返回一個(gè)新的事件流,也就是我們之前的 addOneOrReset$。我們得把這個(gè)“積木”拆開得到 interval,并把時(shí)間數(shù)字傳遞給它。

最后我們可以把原來的 addOneOrReset$ 事件流改造一下:

const addOneOrReset = (time = 1000) =>
      merge(
        interval(time).pipe(
          takeUntil(pauseBtnClick$),
          mapTo(addOne)
        ),
        resetBtnClick$.pipe(mapTo(reset))
      );

把它改造為一個(gè)可以設(shè)置時(shí)間參數(shù)并帶有默認(rèn)值的積木。下面是最終實(shí)現(xiàn)完整代碼:

import React, { useRef, useEffect, useState } from "react";

import { fromEvent, interval, merge } from "rxjs";
import { takeUntil, switchMap, scan, startWith, mapTo } from "rxjs/operators";

export default function App() {
  const [txt, setTxt] = useState("");

  const pauseBtnRef = useRef(null);
  const startBtnRef = useRef(null);
  const resetBtnRef = useRef(null);
  const halfBtnRef = useRef(null);
  const quarterBtnRef = 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 halfBtnClick$ = fromEvent(halfBtnRef.current, "click");
    const quarterBtnClick$ = fromEvent(quarterBtnRef.current, "click");

    const addOneOrReset = (time = 1000) =>
      merge(
        interval(time).pipe(
          takeUntil(pauseBtnClick$),
          mapTo(addOne)
        ),
        resetBtnClick$.pipe(mapTo(reset))
      );
    const time$ = merge(
      startBtnClick$.pipe(mapTo(1000)),
      halfBtnClick$.pipe(mapTo(500)),
      quarterBtnClick$.pipe(mapTo(250))
    );

    const subscription = time$
      .pipe(
        switchMap(addOneOrReset),
        startWith({ count: 0 }),
        scan((acc, current) => current(acc))
      )
      .subscribe(v => setTxt(v.count));

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  return (
    <div className="App">
      <div style={{ fontSize: "30px" }}>{txt}</div>
      <button ref={startBtnRef}>開始</button>
      <button ref={pauseBtnRef}>暫停</button>
      <button ref={resetBtnRef}>重置</button>
      <button ref={halfBtnRef}>1/2秒</button>
      <button ref={quarterBtnRef}>1/4秒</button>
    </div>
  );
}

好了,如有任何問題,請(qǐng)?zhí)砑游⑿殴娞?hào)“讀一讀我”。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容