本篇文章搬家囉! 這裡不再回覆留言,請移至 https://blog.jerry-hong.com/series/rxjs/thirty-days-RxJS-08/
如果是你會如何實作拖拉的功能?
這是【30天精通 RxJS】的 08 篇,如果還沒看過 07 篇可以往這邊走:
30 天精通 RxJS (07): Observable Operators & Marble Diagrams
我們今天要接著講 take, first, takeUntil, concatAll 這四個 operators,並且實作一個簡易的拖拉功能。
take 是一個很簡單的 operator,顧名思義就是取前幾個元素後就結束,範例如下
var source = Rx.Observable.interval(1000);
var example = source.take(3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// complete
這裡可以看到我們的 source
原本是會發出無限個元素的,但這裡我們用 take(3)
就會只取前 3 個元素,取完後就直接結束(complete)。
用 Marble diagram 表示如下
source : -----0-----1-----2-----3--..
take(3)
example: -----0-----1-----2|
first 會取 observable 送出的第 1 個元素之後就直接結束,行為跟 take(1) 一致。
var source = Rx.Observable.interval(1000);
var example = source.first();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// complete
用 Marble diagram 表示如下
source : -----0-----1-----2-----3--..
first()
example: -----0|
在實務上 takeUntil 很常使用到,他可以在某件事情發生時,讓一個 observable 直送出 完成(complete)訊息,範例如下
var source = Rx.Observable.interval(1000);
var click = Rx.Observable.fromEvent(document.body, 'click');
var example = source.takeUntil(click);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// complete (點擊body了
這裡我們一開始先用 interval
建立一個 observable,這個 observable 每隔 1 秒會送出一個從 0 開始遞增的數值,接著我們用 takeUntil
,傳入另一個 observable。
當 takeUntil
傳入的 observable 發送值時,原本的 observable 就會直接進入完成(complete)的狀態,並且發送完成訊息。也就是說上面這段程式碼的行為,會先每 1 秒印出一個數字(從 0 遞增)直到我們點擊 body 為止,他才會送出 complete 訊息。
如果畫成 Marble Diagram 則會像下面這樣
source : -----0-----1-----2------3--
click : ----------------------c----
takeUntil(click)
example: -----0-----1-----2----|
當 click 一發送元素的時候,observable 就會直接完成(complete)。
有時我們的 Observable 送出的元素又是一個 observable,就像是二維陣列,陣列裡面的元素是陣列,這時我們就可以用 concatAll
把它攤平成一維陣列,大家也可以直接把 concatAll 想成把所有元素 concat 起來。
var click = Rx.Observable.fromEvent(document.body, 'click');
var source = click.map(e => Rx.Observable.of(1,2,3));
var example = source.concatAll();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
這個範例我們每點擊一次 body 就會立刻送出 1,2,3,如果用 Marble diagram 表示則如下
click : ------c------------c--------
map(e => Rx.Observable.of(1,2,3))
source : ------o------------o--------
\ \
(123)| (123)|
concatAll()
example: ------(123)--------(123)------------
這裡可以看到 source
observable 內部每次發送的值也是 observable,這時我們用 concatAll 就可以把 source 攤平成 example。
這裡需要注意的是 concatAll
會處理 source 先發出來的 observable,必須等到這個 observable 結束,才會再處理下一個 source 發出來的 observable,讓我們用下面這個範例說明。
var obs1 = Rx.Observable.interval(1000).take(5);
var obs2 = Rx.Observable.interval(500).take(2);
var obs3 = Rx.Observable.interval(2000).take(1);
var source = Rx.Observable.of(obs1, obs2, obs3);
var example = source.concatAll();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// 4
// 0
// 1
// 0
// complete
這裡可以看到 source
會送出 3 個 observable,但是 concatAll
後的行為永遠都是先處理第一個 observable,等到當前處理的結束後才會再處理下一個。
用 Marble diagram 表示如下
source : (o1 o2 o3)|
\ \ \
--0--1--2--3--4| -0-1| ----0|
concatAll()
example: --0--1--2--3--4-0-1----0|
當學完前面幾個 operator 後,我們就很輕鬆地做出拖拉的功能,先讓我們來看一下需求
第一步我已經完成了,大家可以直接到以下兩個連結做練習
第二步我們要先取得各個 DOM 物件,元件(#drag) 跟 body。
const dragDOM = document.getElementById('drag');
const body = document.body;
要取得 body 的原因是因為滑鼠移動(mousemove)跟滑鼠左鍵放掉(mouseup)都應該是在整個 body 監聽。
第三步我們寫出各個會用到的監聽事件,並用 fromEvent
來取得各個 observable。
const mouseDown = Rx.Observable.fromEvent(dragDOM, 'mousedown');
const mouseUp = Rx.Observable.fromEvent(body, 'mouseup');
const mouseMove = Rx.Observable.fromEvent(body, 'mousemove');
記得還沒
subscribe
之前都不會開始監聽,一定會等到 subscribe 之後 observable 才會開始送值。
第四步開始寫邏輯
當 mouseDown 時,轉成 mouseMove 的事件
const source = mouseDown.map(event => mouseMove)
mouseMove 要在 mouseUp 後結束
加上 takeUntil(mouseUp)
const source = mouseDown
.map(event => mouseMove.takeUntil(mouseUp))
這時 source 大概長像這樣
source: -------e--------------e-----
\ \
--m-m-m-m| -m--m-m--m-m|
m 代表 mousemove event
用 concatAll()
攤平 source 成一維。
const source = mouseDown
.map(event => mouseMove.takeUntil(mouseUp))
.concatAll();
用 map 把 mousemove event 轉成 x,y 的位置,並且訂閱。
source
.map(m => {
return {
x: m.clientX,
y: m.clientY
}
})
.subscribe(pos => {
dragDOM.style.left = pos.x + 'px';
dragDOM.style.top = pos.y + 'px';
})
到這裡我們就已經完成了簡易的拖拉功能了!完整的程式碼如下
const dragDOM = document.getElementById('drag');
const body = document.body;
const mouseDown = Rx.Observable.fromEvent(dragDOM, 'mousedown');
const mouseUp = Rx.Observable.fromEvent(body, 'mouseup');
const mouseMove = Rx.Observable.fromEvent(body, 'mousemove');
mouseDown
.map(event => mouseMove.takeUntil(mouseUp))
.concatAll()
.map(event => ({ x: event.clientX, y: event.clientY }))
.subscribe(pos => {
dragDOM.style.left = pos.x + 'px';
dragDOM.style.top = pos.y + 'px';
})
不知道讀者有沒有感受到,我們整個程式碼不到 15 行,而且只要能夠看懂各個 operators,我們程式可讀性是非常的高。
雖然這只是一個簡單的拖拉實現,但已經展示出 RxJS 帶來的威力,它讓我們的程式碼更加的簡潔,也更好的維護!
這裡有完整的成果可以參考。
我們今天介紹了四個 operators 分別是 take, first, takeUntil, concatAll,並且完成了一個簡易的拖拉功能,我們之後會把這個拖拉功能做得更完整,並且整合其他功能!
不知道讀者今天有沒有收穫?如果有任何問題,歡迎在下方留言給我!
如果你喜歡這篇文章,請至標題旁幫我按個 星星+like,謝謝。
這一行太厲害了!!mouseDown.map(event => mouseMove.takeUntil(mouseUp))
哈哈 對啊
期待之後的應用,太強了
沒想到 event 也能這樣轉!?太厲害!
太神啦!!
Rx和事件結合後感覺很棒壓~
作者繼續加油啊!
聖誕快樂XD
聖誕快樂?
怎麼變問號 手機版留言不能用 emoji 呼叫小財神
推好文!
如果單純是要寫功能,用switchMap或switchMapTo應該會更簡潔?如下,concatAll就可以刪掉,而且邏輯上也單純是從一個stream切換到另一個stream,不過我猜作者是想要介紹concatAll?
mouseDown
.switchMapTo(mouseMove.takeUntil(mouseUp))
.map(event => ({ x: event.clientX, y: event.clientY }))
.subscribe(pos => {
dragDOM.style.left = pos.x + 'px';
dragDOM.style.top = pos.y + 'px';
})
這篇文章主要是想延續 30 天精通 RxJS (03): Functional Programming 通用函式 這篇所寫的。
主要希望能讓初學者在學過陣列的操作後,再來看 observable 會比較容易接受,又因為陣列只能實作 concatAll,所以這裡就用 concatAll 讓讀者能夠延續前面學的,感受會比較深。
另外今天的文章就會寫到 concatMap、mergeMap 跟 switchMap 喔!
版主您好,想請問一下:
如果事件幫給 fromEvent(parent) 但點到 child element move 時 e.ClientX/Y 的相對位置錯誤有什麼原因呢?看了 useCapture 但是 selector 那邊不太會用。
不好意思 我還是有點不瞭解
mouseDown.map(event => mouseMove.takeUntil(mouseUp))
我的解讀是
mouseDown每次觸發時都會回傳(mouseMove直到mouseUp)這個observable
但是這行為什麼不行呢?
mouseDown.map(event => mouseMove).takeUntil(mouseUp)
我的解讀是
mouseDown每次觸發時都會回傳mouseMove這個observable
這個observable會取到mouseUp為止
mouseDown.map(event => mouseMove).takeUntil(mouseUp)
這樣寫會變成
mouseDown 事件監聽到 mouseUp 就結束監聽
以 Observable 的角度來解釋就是,這樣的寫法 mouseUp 時結束的是 outerObservable(mouseDown) 不是 innerObservable(mouseMove)
用 Marble Daigram 畫出來就是
mouseDown: ------o-------o------
\
---e--e--ee-e-e--...
takeUntil
mouseUp: ----------e----------
mouseDown: ------o---|
\
---e--e--ee-e-e--...
您好,想請教如下
如果 要增加 事件 takeUntil(mouseOut) 不知道要加在哪裡
想要讓他 也可以在超出body時 也停止mouseMove
const mouseOut = Rx.Observable.fromEvent(body, 'mouseout');
takeUntil(mouseOut)
本篇文章搬家囉! 這裡不再回覆留言,請移至 https://blog.jerry-hong.com/series/rxjs/thirty-days-RxJS-08/