RxJS 實現摩斯密碼(Morse) 【內附腦圖】

參加 2018 ngChina 開發者大會,特別喜歡 Michael Hladky 奧地利帥哥的 RxJS 分享,如今拿出來好好學習工做坊的內容(工做坊Demo地址),結合這個示例,作了一個改進版本,實現更簡潔,邏輯更直觀。數組

1、摩斯密碼是什麼?

瞭解者可跳過次章節緩存

摩斯密碼(Morse),是一種時通時斷的信號代碼,這種信號代碼經過不一樣的排列組合來表達不一樣的英文字母、數字和標點符號等。
地球人都知道的 SOS 求救信號,就是 Morse,三短(S) 三長(O) 三短(S)。less

信號對應表以下:函數

 

image.png

 

2、業務邏輯分析

分析關鍵步驟,很巧,和把大象裝進冰箱裏一樣都只須要三步耶:學習

第一步,識別點信號,短爲 「滴」 長爲「嗒」。優化

第二步,根據 「長間隔」 來切片分組。this

第三步,分組數據根據對應錶轉化出最終結果。blog

3、擼代碼,優化後版本(完整在線示例)

開始前要作好熱身活動:事件

Morse 的最小單元,"." 表明嘀,"-" 表明嗒,點擊事件用 Down 表明 mousedown,Up 表明 mouseup。
200ms 間隔用來區別嘀嗒,1s 間隔用來區分一個 Morse 單元組的結束。ip

 // 點信號 = Down - Up = 間隔 < 200ms ?"." : "-"; 
// Down <200ms Up >1s = "." = E
// Down <200ms Up <1s Down >200ms Up >1s = ".", "-" = A

// 直接使用 fromEvent 操做符,來生成點擊操做的流,而後用 map 操做符轉化成時間戳,
// takeUntil 用來控制流的結束,避免重複訂閱。

const clickBegin$ = fromEvent(this.sendButtonElementRef.nativeElement, 'mousedown')
  .pipe(
    takeUntil(this.onDestroy$),
    map(n => Date.now())
  )
const clickEnd$ = fromEvent(this.sendButtonElementRef.nativeElement, 'mouseup')
  .pipe(
    takeUntil(this.onDestroy$),
    map(n => Date.now())
  )
第一步,識別點信號爲 「滴」 「嗒」

前面代碼已經拿到點擊事件的流,而且用 "map" 操做符,把數據轉化爲當前的時間戳。

下面開始計算 Down & Up 之間的間隔時間,思考,合併兩個流的的操做符有哪些呢?

  1. forkJoin、concat ?
    須要兩個流 complate 狀態後才返回數據,不適應數據持續輸出的場景。

  2. merge ?
    Down & Up 的時間戳不會同時得到,還須要處理存儲的問題,不徹底適應場景。

  3. combineLatest ?
    知足數據持續輸出,知足同時得到,哎喲,還不錯。
    可是這個操做符的特色是,會緩存上一次的值,因此第二次 Down 也會得到到數據,Up - Down 也就會爲負值,取絕對值後能夠用來判斷是否 >1s,來區分一個 Morse 單元組的結束。

  4. zip ?
    哎呀哈,這個更合適呢,盤它!
    單詞選的很到位,這個操做符功能能夠理解爲像拉鍊同樣,確保得到數據每一次都是一個純淨的 Down & Up。
    可是須要注意 zip 會自動緩存數據,例如 zip(A$, B$),A$收到的數據一直比B$多太多,有內存溢出風險,就像拉錯位的拉鍊,很藍瘦。

 // zip的實現
zip(clickBegin$, clickEnd$)
    .pipe(
    // 計算 Down - Up 間隔時間
    map(this.toTimeDiff),
    // 根據間隔時間,轉化爲嘀嗒替代字符 "." "-"
    map(this.msToMorseCharacter)
    )
    .subscribe(result => {
      // 發送到主信號流
      morseSignal$.next(result);
    });
第二步,根據 「長間隔」 來切片分組

分組的操做符有哪些?

  1. partition?
    根據函數拆成兩個流。

  2. groupBy?
    根據函數拆成 n 個流。

  3. window?
    根據流拆成 n 個流。以上各位都打擾了,我還要本身處理數據緩存,再見。

  4. buffer?
    哇,初戀般的感受,用流控制來作切片數據成數組,拿到數組只須要 join 一下就好,就能夠去去匹配對應表了,好棒!
    「長間隔」的切片流,怎麼得到呢?拿出法寶 debounceTime(1000) ,當點擊的 Down Up 週期完成後,間隔 1s 就認爲是一個Morse 單元組的結束。
    而後又遇到了問題,怎麼判斷一個點擊週期呢?不用單純用 Up ,由於下一個 Down Up 週期可能會超出 1s,就會致使切片時機錯誤。因此模擬了點擊持續的流 clickKeeping$,用 switchMap 替換爲新的流且不影響原來的流,timer 產生一個小於 1s 間隔的持續流信號,用 takeUntil 在 Up 事件流 clickEnd$ 後把整個流結束。

 // 點擊持續狀態流
const clickKeeping$ = clickBegin$
    .pipe(
        // 替換爲新的流,不影響原來的流
        switchMap(() => {
            // 定時在持續發送數據,維持點擊中狀態
            return timer(0, morseTimeRanges.lessThenlongBreak).pipe(
                // 直到 Up 後結束點擊狀態
                takeUntil(clickEnd$)
            );
        })
    )

// 「長間隔」的切片流
const morseBreak$ = clickKeeping$.pipe(
    debounceTime(morseTimeRanges.longBreak)
);

// 得到 Morse 單元組
morseSignal$
    .pipe(
        // 切片分組主信號流
        buffer(morseBreak$) // 轉化爲,例如 ['.', '.', '.']
    )
第三步,分組數據根據對應錶轉化出最終結果

join('') Morse 單元組去匹配對應表,很簡單不用說。

錯誤發生在 switchMap 中,分支流報錯,可是主流不會收到影響,而後用 catchError 捕捉錯誤。

 // Morse 單元組去匹配對應表
private translateSymbolToLetter = morseArray => {
    const morseCharacters = morseArray.join('');
    const find = morseTranslations.find(n => n.symbol === morseCharacters)
    // 這裏 find 可能爲 undefined 致使報錯,可是錯誤會被 catchError 捕捉
    return find.char;
}

// 轉化+錯誤處理,最終完成
morseSignal$
    .pipe(
        buffer(morseBreak$),
        switchMap(n => {
            return of(n).pipe(
                // 只爲了 Demo 演示中的展現用
                tap(n => this.lastMorseGroupCharacters = n.join(' ')),
                // 轉化成對應表中字符
                map(this.translateSymbolToLetter),
                // 捕捉錯誤
                catchError(n => {
                    return of(morseCharacters.errorString);
                })
            )
        })
    )
    .subscribe(result => {
        // 輸出最終轉化結果
        this.morseLog.push(result);
        console.log('結果:', result)
    });

4、解讀 Michael Hladky 大神的示例

總體上,把 「嘀嗒」 「短間隔」 「長間隔」 都轉化成替代符,過濾無用的替代符,而後 filter 「長間隔」 替代符的流,來作 buffer 切片數據。其餘還有由於使用 combineLatest 操做符致使的不一樣。

// 識別 「嘀」 「嗒」
const morseCharFromEvents$ = observableCombineLatest(this.startEvents$, this.stopEvents$)
    .pipe(
        // 計算 mousedown mouseup 時間間隔
        map(this.toTimeDiff),
        // 轉化成標識符
        map(this.msToMorseChar),
        // 過濾 Morse 單元組中的 「短間隔「 標識符
        filter(this.isCharNoShortBreak as any)
    );

// 主信號流
this.morseChar$ = observableMerge(morseCharFromEvents$, this.injectMorseChar$)

// 識別 「長間隔「 標識符,來做爲切片流
const longBreaks$ = this.morseChar$
    .pipe(filter(this.isCharLongBreak as any));

// 切片成 Morse 單元組
this.morseSymbol$ = this.morseChar$
    .pipe(
        buffer(longBreaks$),
        map(this.charArrayToSymbol),
        filter(n => (n !== '') as any)
    )

// 錯誤處理 + 標識符對應錶轉化
this.morseLetter$ = this.morseSymbol$
    .pipe(
        switchMap(n => observableOf(n).pipe(this.saveTranslate('ERROR')))
    );

// Up 後補4個 「長間隔「 標識符,用來作 Morse 單元組的結束
const breakEmitter$ = observableTimer(this.msLongBreak, this.msLongBreak)
    .pipe(
        mapTo(this.mC.longBreak),
        take(4)
    );
this.stopEventsSubject
    .pipe(
        switchMapTo(
          breakEmitter$.pipe(takeUntil(this.startEventsSubject))
        )
    )
    .subscribe(n => this.injectMorseChar(n));

總結

下圖是讀完《深刻淺出RxJS》後的學習筆記,標註了一些操做符的快速記憶特色,方便使用的適合查閱。

 

image.png

 

本文做者:甄帥

文章來源:Worktile技術博客

歡迎訪問交流更多關於技術及協做的問題。

文章轉載請註明出處。

相關文章
相關標籤/搜索