當前位置:
首頁 > 知識 > RxJS 實現摩斯密碼(Morse) 【內附腦圖】

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

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

一、摩斯密碼是什麼?


了解者可跳過次章節

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

信號對應表如下:

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

打開今日頭條,查看更多圖片

二、業務邏輯分析

分析關鍵步驟,很巧,和把大象裝進冰箱里同樣都只需要三步耶:

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

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

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

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

開始前要做好熱身活動:

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

// 點信號 = 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)
});

四、解讀 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》後的學習筆記,標註了一些操作符的快速記憶特點,方便使用的適合查閱。

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

作者:

原文:https://my.oschina.net/worktile/blog/3011746

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

Qt通過事務讀寫操作多個資料庫
一步步封裝實現自己的網路請求框架

TAG:程序員小新人學習 |