前言
很長一段時間沒有寫 Angular 了 (哎...全棧的命), 近期計劃又要開始回去寫了. 於是就開始做複習咯.
我的複習是從 JS > TS > RxJS > Angular, 與此同時當然是順便寫一系列半教程半複習的文章咯, 我當年寫了許多 Angular 的學習筆記, 只是文筆太爛, 這次得好好寫了.
JS 已經複習到七七八八了, TS 老是提不起勁去寫, 所以就改成邊寫 TS 邊寫 RxJS 吧.
以前寫過相關的文章:
angular2 學習筆記 ( Rxjs, Promise, Async/Await 的區別 )
什麼是流 (stream) ?
RxJS 參雜了許多概念, 什麼函數式, 觀察者, 異步等等...
但我個人覺得最容易理解的部分是 stream (流).
stream 是什麼? 它表示一個時間內一個變化的狀態.
在 JS 裏, 狀態可以被理解爲某個值, variable 的值.
時間則是用戶使用 App 的時間.
看例子吧:
上圖 (gif) 大概是 5 秒鐘, 這個就是時間. 在這 5 秒中裏面, 價錢 (值) 變化了好幾次 (160, 190, 200, 250)
一個有時間, 有變化的值就可以瞭解爲一個 stream.
Why Stream? Because... 管理
爲什麼要用 "stream" 概念去理解這些 "值" ? 不能簡單的理解爲 "點擊" > "更新 value" ?
當然可以, 其實 stream 概念並不是爲了理解, 而是爲了管理.
當程序裏出現越來越多, 變來變去的值以後, 出現 bug 的機率就越來越高, 而追蹤 bug 也越來越喫力.
所以就必須整一套概念來管理它們. 這就好比你用 Redux 來管理 React 的 state 一樣.
以前有許多人拿 redux 去管理簡單的程序, 結果就是大材小用, 反而是 redux 本身增加了整個系統的複雜度...幸好後來出現了 hook 才把這羣人拉了出來...(永遠記得, 軟件開發一定要看清楚當前項目需求, 選擇合適的方案而不是最牛逼的方案)
觀察者模式
上面提到了, stream 的其中一個特色就是變化. 一個東西變化了, 那麼依賴它的東西通常也會跟着變化. 蝴蝶效應...
我們在寫 Excel 的時候經常會寫這樣的邏輯 cell
full name 這個值, 來自 first name + ' ' + last name
而每當 first name 或 last name 變化以後, full name 也隨之變化.
在上面這個例子裏, first name, last name 就是 stream. 隨着時間它會發生變化.
而 full name 算是一個 depend and addon stream. 它也會變化, 同時它依賴其它的 stream 和一些而外的處理邏輯.
用 RxJS 來表達這類型的場景會非常貼切.
體驗一下:
Without RxJS 實現:
const firstName = document.querySelector<HTMLInputElement>('#first-name')!; const lastName = document.querySelector<HTMLInputElement>('#last-name')!; const fullName = document.querySelector<HTMLSpanElement>('#full-name')!; for (const input of [firstName, lastName]) { input.addEventListener('input', () => { fullName.textContent = `${firstName.value} ${lastName.value}`; }); }
用 RxJS 來實現:
const firstNameInput = document.querySelector<HTMLInputElement>('#first-name')!; const lastNameInput = document.querySelector<HTMLInputElement>('#last-name')!; const fullNameSpan = document.querySelector<HTMLSpanElement>('#full-name')!; // 表達 stream const firstName$ = fromEvent(firstNameInput, 'input').pipe( map(() => firstNameInput.value), startWith(firstNameInput.value) ); const lastName$ = fromEvent(lastNameInput, 'input').pipe( map(() => lastNameInput.value), startWith(lastNameInput.value) ); const fullName$ = combineLatest([firstName$, lastName$]).pipe( map(([firstName, lastName]) => `${firstName} ${lastName}`) ); // 消費 stream fullName$.subscribe(fullName => { fullNameSpan.textContent = fullName; });
哇...怎麼更復雜了...所以啊, 上面說了, 程序簡單就沒必要搞 RxJS 啊.
但你看看它的管理是不錯的, 表達 stream 負責描述 stream 的來源.
尤其是那個 combine stream 的表達尤其加分.
消費 stream 則可以做許多事情 (比如 render view).
這樣 stream 可以被多個地方複用.
贈送一個優化版本:
// 這個可以封裝起來 function fromInput(input: HTMLInputElement): Observable<string> { return fromEvent(input, 'input').pipe( map(() => input.value), startWith(input.value) ); } // 表達 stream const firstName$ = fromInput(document.querySelector<HTMLInputElement>('#first-name')!); const lastName$ = fromInput(document.querySelector<HTMLInputElement>('#last-name')!); const fullName$ = combineLatest([firstName$, lastName$]).pipe( map(([firstName, lastName]) => `${firstName} ${lastName}`) ); // 消費 stream const fullNameSpan = document.querySelector<HTMLSpanElement>('#full-name')!; fullName$.subscribe(fullName => { fullNameSpan.textContent = fullName; // render view });