如果是你會如何實作拖拉的功能?
今天建議大家直接看影片
我們今天要接著講 take, first, takeUntil, concatAll 這四個 operators,並且實作一個簡易的拖拉功能。

Operators

take

take 是一個很簡單的 operator,顧名思義就是取前幾個元素後就結束,範例如下

  1. var source = Rx.Observable.interval(1000);
  2. var example = source.take(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
  9. // 1
  10. // 2
  11. // complete

這裡可以看到我們的 source 原本是會發出無限個元素的,但這裡我們用 take(3) 就會只取前 3 個元素,取完後就直接結束(complete)。
用 Marble diagram 表示如下

  1. source : -----0-----1-----2-----3--..
  2. take(3)
  3. example: -----0-----1-----2|

first

first 會取 observable 送出的第 1 個元素之後就直接結束,行為跟 take(1) 一致。

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

用 Marble diagram 表示如下

  1. source : -----0-----1-----2-----3--..
  2. first()
  3. example: -----0|

takeUntil

在實務上 takeUntil 很常使用到,他可以在某件事情發生時,讓一個 observable 直送出 完成(complete)訊息,範例如下

  1. var source = Rx.Observable.interval(1000);
  2. var click = Rx.Observable.fromEvent(document.body, 'click');
  3. var example = source.takeUntil(click);
  4. example.subscribe({
  5. next: (value) => { console.log(value); },
  6. error: (err) => { console.log('Error: ' + err); },
  7. complete: () => { console.log('complete'); }
  8. });
  9. // 0
  10. // 1
  11. // 2
  12. // 3
  13. // complete (點擊body了

這裡我們一開始先用 interval 建立一個 observable,這個 observable 每隔 1 秒會送出一個從 0 開始遞增的數值,接著我們用 takeUntil,傳入另一個 observable。
takeUntil 傳入的 observable 發送值時,原本的 observable 就會直接進入完成(complete)的狀態,並且發送完成訊息。也就是說上面這段程式碼的行為,會先每 1 秒印出一個數字(從 0 遞增)直到我們點擊 body 為止,他才會送出 complete 訊息。
如果畫成 Marble Diagram 則會像下面這樣

  1. source : -----0-----1-----2------3--
  2. click : ----------------------c----
  3. takeUntil(click)
  4. example: -----0-----1-----2----|

當 click 一發送元素的時候,observable 就會直接完成(complete)。

concatAll

有時我們的 Observable 送出的元素又是一個 observable,就像是二維陣列,陣列裡面的元素是陣列,這時我們就可以用 concatAll 把它攤平成一維陣列,大家也可以直接把 concatAll 想成把所有元素 concat 起來。

  1. var click = Rx.Observable.fromEvent(document.body, 'click');
  2. var source = click.map(e => Rx.Observable.of(1,2,3));
  3. var example = source.concatAll();
  4. example.subscribe({
  5. next: (value) => { console.log(value); },
  6. error: (err) => { console.log('Error: ' + err); },
  7. complete: () => { console.log('complete'); }
  8. });

這個範例我們每點擊一次 body 就會立刻送出 1,2,3,如果用 Marble diagram 表示則如下

  1. click : ------c------------c--------
  2. map(e => Rx.Observable.of(1,2,3))
  3. source : ------o------------o--------
  4. \ \
  5. (123)| (123)|
  6. concatAll()
  7. example: ------(123)--------(123)------------

這裡可以看到 source observable 內部每次發送的值也是 observable,這時我們用 concatAll 就可以把 source 攤平成 example。
這裡需要注意的是 concatAll 會處理 source 先發出來的 observable,必須等到這個 observable 結束,才會再處理下一個 source 發出來的 observable,讓我們用下面這個範例說明。

  1. var obs1 = Rx.Observable.interval(1000).take(5);
  2. var obs2 = Rx.Observable.interval(500).take(2);
  3. var obs3 = Rx.Observable.interval(2000).take(1);
  4. var source = Rx.Observable.of(obs1, obs2, obs3);
  5. var example = source.concatAll();
  6. example.subscribe({
  7. next: (value) => { console.log(value); },
  8. error: (err) => { console.log('Error: ' + err); },
  9. complete: () => { console.log('complete'); }
  10. });
  11. // 0
  12. // 1
  13. // 2
  14. // 3
  15. // 4
  16. // 0
  17. // 1
  18. // 0
  19. // complete

這裡可以看到 source 會送出 3 個 observable,但是 concatAll 後的行為永遠都是先處理第一個 observable,等到當前處理的結束後才會再處理下一個。
用 Marble diagram 表示如下

  1. source : (o1 o2 o3)|
  2. \ \ \
  3. --0--1--2--3--4| -0-1| ----0|
  4. concatAll()
  5. example: --0--1--2--3--4-0-1----0|

簡易拖拉

當學完前面幾個 operator 後,我們就很輕鬆地做出拖拉的功能,先讓我們來看一下需求

  1. 首先畫面上有一個元件(#drag)
  2. 當滑鼠在元件(#drag)上按下左鍵(mousedown)時,開始監聽滑鼠移動(mousemove)的位置
  3. 當滑鼠左鍵放掉(mouseup)時,結束監聽滑鼠移動
  4. 當滑鼠移動(mousemove)被監聽時,跟著修改元件的樣式屬性

第一步我已經完成了,大家可以直接到以下兩個連結做練習
JSBin
JSFiddle
第二步我們要先取得各個 DOM 物件,元件(#drag) 跟 body。

  1. const dragDOM = document.getElementById('drag');
  2. const body = document.body;

要取得 body 的原因是因為滑鼠移動(mousemove)跟滑鼠左鍵放掉(mouseup)都應該是在整個 body 監聽。
第三步我們寫出各個會用到的監聽事件,並用 fromEvent 來取得各個 observable。

  • 對 #drag 監聽 mousedown
  • 對 body 監聽 mouseup
  • 對 body 監聽 mousemove
  1. const mouseDown = Rx.Observable.fromEvent(dragDOM, 'mousedown');
  2. const mouseUp = Rx.Observable.fromEvent(body, 'mouseup');
  3. const mouseMove = Rx.Observable.fromEvent(body, 'mousemove');

記得還沒 subscribe 之前都不會開始監聽,一定會等到 subscribe 之後 observable 才會開始送值。

第四步開始寫邏輯
當 mouseDown 時,轉成 mouseMove 的事件

  1. const source = mouseDown.map(event => mouseMove)

mouseMove 要在 mouseUp 後結束
加上 takeUntil(mouseUp)

  1. const source = mouseDown
  2. .map(event => mouseMove.takeUntil(mouseUp))

這時 source 大概長像這樣

  1. source: -------e--------------e-----
  2. \ \
  3. --m-m-m-m| -m--m-m--m-m|

m 代表 mousemove event

concatAll() 攤平 source 成一維。

  1. const source = mouseDown
  2. .map(event => mouseMove.takeUntil(mouseUp))
  3. .concatAll();
  1. <br />用 map 把 mousemove event 轉成 x,y 的位置,並且訂閱。
  1. source
  2. .map(m => {
  3. return {
  4. x: m.clientX,
  5. y: m.clientY
  6. }
  7. })
  8. .subscribe(pos => {
  9. dragDOM.style.left = pos.x + 'px';
  10. dragDOM.style.top = pos.y + 'px';
  11. });

到這裡我們就已經完成了簡易的拖拉功能了!完整的程式碼如下

  1. const dragDOM = document.getElementById('drag');
  2. const body = document.body;
  3. const mouseDown = Rx.Observable.fromEvent(dragDOM, 'mousedown');
  4. const mouseUp = Rx.Observable.fromEvent(body, 'mouseup');
  5. const mouseMove = Rx.Observable.fromEvent(body, 'mousemove');
  6. mouseDown
  7. .map(event => mouseMove.takeUntil(mouseUp))
  8. .concatAll()
  9. .map(event => ({ x: event.clientX, y: event.clientY }))
  10. .subscribe(pos => {
  11. dragDOM.style.left = pos.x + 'px';
  12. dragDOM.style.top = pos.y + 'px';
  13. })

不知道讀者有沒有感受到,我們整個程式碼不到 15 行,而且只要能夠看懂各個 operators,我們程式可讀性是非常的高。
雖然這只是一個簡單的拖拉實現,但已經展示出 RxJS 帶來的威力,它讓我們的程式碼更加的簡潔,也更好的維護!

這裡有完整的成果可以參考。