今天要繼續講兩個簡單的 transformation operators 並帶一些小範例,這兩個 operators 都是實務上很常會用到的方法。

Operators

scan

scan 其實就是 Observable 版本的 reduce 只是命名不同。如果熟悉陣列操作的話,應該會知道原生的 JS Array 就有 reduce 的方法,使用方式如下

  1. var arr = [1, 2, 3, 4];
  2. var result = arr.reduce((origin, next) => {
  3. console.log(origin)
  4. return origin + next
  5. }, 0);
  6. console.log(result)
  7. // 0
  8. // 1
  9. // 3
  10. // 6
  11. // 10

reduce 方法需要傳兩個參數,第一個是 callback 第二個則是起始狀態,這個 callback 執行時,會傳入兩個參數一個是原本的狀態,第二個是修改原本狀態的參數,最後回傳一個新的狀態,再繼續執行。
所以這段程式碼是這樣執行的

  • 第一次執行 callback 起始狀態是 0 所以 origin 傳入 0,next 為 arr 的第一個元素 1,相加之後變成 1 回傳並當作下一次的狀態。
  • 第二次執行 callback,這時原本的狀態(origin)就變成了 1,next 為 arr 的第二個元素 2,相加之後變成 3 回傳並當作下一次的狀態。
  • 第三次執行 callback,這時原本的狀態(origin)就變成了 3,next 為 arr 的第三個元素 3,相加之後變成 6 回傳並當作下一次的狀態。
  • 第三次執行 callback,這時原本的狀態(origin)就變成了 6,next 為 arr 的第四個元素 4,相加之後變成 10 回傳並當作下一次的狀態。
  • 這時 arr 的元素都已經遍歷過了,所以不會直接把 10 回傳。

scan 整體的運作方式都跟 reduce 一樣,範例如下

  1. var source = Rx.Observable.from('hello')
  2. .zip(Rx.Observable.interval(600), (x, y) => x);
  3. var example = source.scan((origin, next) => origin + next, '');
  4. example.subscribe({
  5. next: (value) => { console.log(value); },
  6. error: (err) => { console.log('Error: ' + err); },
  7. complete: () => { console.log('complete'); }
  8. });
  9. // h
  10. // he
  11. // hel
  12. // hell
  13. // hello
  14. // complete

畫成 Marble Diagram

  1. source : ----h----e----l----l----o|
  2. scan((origin, next) => origin + next, '')
  3. example: ----h----(he)----(hel)----(hell)----(hello)|

這裡可以看到第一次傳入 'h''' 相加,返回 'h' 當作下一次的初始狀態,一直重複下去。

scan 跟 reduce 最大的差別就在 scan 一定會回傳一個 observable 實例,而 reduce 最後回傳的值有可能是任何資料型別,必須看使用者傳入的 callback 才能決定 reduce 最後的返回值。

Jafar Husain 就曾說:「JavaScript 的 reduce 是錯了,它最後應該永遠回傳陣列才對!」

如果大家之前有到這裡練習的話,會發現 reduce 被設計成一定回傳陣列,而這個網頁就是 Jafar 做的。

scan 很常用在狀態的計算處理,最簡單的就是對一個數字的加減,我們可以綁定一個 button 的 click 事件,並用 map 把 click event 轉成 1,之後送處 scan 計算值再做顯示。
這裡筆者寫了一個小範例,來示範如何做最簡單的加減

  1. const addButton = document.getElementById('addButton');
  2. const minusButton = document.getElementById('minusButton');
  3. const state = document.getElementById('state');
  4. const addClick = Rx.Observable.fromEvent(addButton, 'click').mapTo(1);
  5. const minusClick = Rx.Observable.fromEvent(minusButton, 'click').mapTo(-1);
  6. const numberState = Rx.Observable.empty()
  7. .startWith(0)
  8. .merge(addClick, minusClick)
  9. .scan((origin, next) => origin + next, 0)
  10. numberState
  11. .subscribe({
  12. next: (value) => { state.innerHTML = value;},
  13. error: (err) => { console.log('Error: ' + err); },
  14. complete: () => { console.log('complete'); }
  15. });

這裡我們用了兩個 button,一個是 add 按鈕,一個是 minus 按鈕。
我把這兩個按鈕的點擊事件各建立了 addClcik, minusClick 兩個 observable,這兩個 observable 直接 mapTo(1) 跟 mapTo(-1),代表被點擊後會各自送出的數字!
接著我們用了 empty() 建立一個空的 observable 代表畫面上數字的狀態,搭配 startWith(0) 來設定初始值,接著用 merge 把兩個 observable 合併透過 scan 處理之後的邏輯,最後在 subscribe 來更改畫面的顯示。
這個小範例用到了我們這幾天講的 operators,包含 mapTo, empty, startWith, merge 還有現在講的 scan,建議讀者一定要花時間稍微練習一下。

buffer

buffer 是一整個家族,總共有五個相關的 operators

  • buffer
  • bufferCount
  • bufferTime
  • bufferToggle
  • bufferWhen

這裡比較常用到的是 buffer, bufferCount 跟 bufferTime 這三個,我們直接來看範例。

  1. var source = Rx.Observable.interval(300);
  2. var source2 = Rx.Observable.interval(1000);
  3. var example = source.buffer(source2);
  4. example.subscribe({
  5. next: (value) => { console.log(value); },
  6. error: (err) => { console.log('Error: ' + err); },
  7. complete: () => { console.log('complete'); }
  8. });
  9. // [0,1,2]
  10. // [3,4,5]
  11. // [6,7,8]...

畫成 Marble Diagram 則像是

  1. source : --0--1--2--3--4--5--6--7..
  2. source2: ---------0---------1--------...
  3. buffer(source2)
  4. example: ---------([0,1,2])---------([3,4,5])

buffer 要傳入一個 observable(source2),它會把原本的 observable (source)送出的元素緩存在陣列中,等到傳入的 observable(source2) 送出元素時,就會觸發把緩存的元素送出。
這裡的範例 source2 是每一秒就會送出一個元素,我們可以改用 bufferTime 簡潔的表達,如下

  1. var source = Rx.Observable.interval(300);
  2. var example = source.bufferTime(1000);
  3. example.subscribe({
  4. next: (value) => { console.log(value); },
  5. error: (err) => { console.log('Error: ' + err); },
  6. complete: () => { console.log('complete'); }
  7. });
  8. // [0,1,2]
  9. // [3,4,5]
  10. // [6,7,8]...

除了用時間來作緩存外,我們更常用數量來做緩存,範例如下

  1. var source = Rx.Observable.interval(300);
  2. var example = source.bufferCount(3);
  3. example.subscribe({
  4. next: (value) => { console.log(value); },
  5. error: (err) => { console.log('Error: ' + err); },
  6. complete: () => { console.log('complete'); }
  7. });
  8. // [0,1,2]
  9. // [3,4,5]
  10. // [6,7,8]...

在實務上,我們可以用 buffer 來做某個事件的過濾,例如像是滑鼠連點才能真的執行,這裡我們一樣寫了一個小範例

  1. const button = document.getElementById('demo');
  2. const click = Rx.Observable.fromEvent(button, 'click')
  3. const example = click
  4. .bufferTime(500)
  5. .filter(arr => arr.length >= 2);
  6. example.subscribe({
  7. next: (value) => { console.log('success'); },
  8. error: (err) => { console.log('Error: ' + err); },
  9. complete: () => { console.log('complete'); }
  10. });

這裡我們只有在 500 毫秒內連點兩下,才能成功印出 ‘success’,這個功能在某些特殊的需求中非常的好用,也能用在批次處理來降低 request 傳送的次數!