RxJS 系列 – 概念篇

前言

很長一段時間沒有寫 Angular 了 (哎...全棧的命), 近期計劃又要開始回去寫了. 於是就開始做複習咯.

我的複習是從 JS > TS > RxJS > Angular, 與此同時當然是順便寫一系列半教程半複習的文章咯, 我當年寫了許多 Angular 的學習筆記, 只是文筆太爛, 這次得好好寫了.

JS 已經複習到七七八八了, TS 老是提不起勁去寫, 所以就改成邊寫 TS 邊寫 RxJS 吧.

 

以前寫過相關的文章:

angular2 學習筆記 ( Rxjs, Promise, Async/Await 的區別 )

angular2 學習筆記 ( rxjs 流 )

 

什麼是流 (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
});

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章