RxJS 實戰篇(一)拖拽

本文最初發佈於個人我的博客:咀嚼之味javascript

面對交互性很強、數據變化複雜的場景,傳統的前端開發方式每每存在一些共有的問題:1). UI 狀態與數據難以追蹤;2). 寫出的代碼可讀性不好,邏輯代碼分佈離散。css

相比之下,響應式編程(Reactive Programming)在解決此類問題上有着得天獨厚的優點。Vue、Mobx、RxJS 這些庫都是響應式編程思想的結晶。html

不少人在接觸到 RxJS 後會有一個共同的感受:這個庫雖然很強大,但奈何各類各樣的 operators 太多了,在實際場景中根本不知道怎麼運用!因此本文並不旨在闡釋響應式編程的優越性,而是經過按部就班的實例來展現 RxJS 經常使用 operators 的使用場景。若是你還沒有入門 RxJS,推薦你能夠先看看一位來自臺灣的前端工程師 Jerry Hong 寫的 30 天精通 RxJS 系列。不要被三十天這個標題給嚇到啦,若是你有一些函數式編程的經驗的話,週末花一天時間就能看完。固然要加深對 RxJS 的理解仍是得多多實戰。畢竟實踐出真知嘛!前端

本文不適合 未入門的新手已精通的高手。若是你以爲你對 RxJS 有了初步的認識,但掌握程度不高,可能這篇文章就比較適合你了。你能夠嘗試跟着本文的三個實例本身先作作看,再對比一下本文給出的解決方案,相信你能對 RxJS 有更深刻的理解。注意,本文給出的解決方案並不必定是最優的解決方案,若是你有什麼改進的建議,能夠在文末留言,謝謝!java

1. 簡單的拖拽

需求:給定一個小方塊,實現簡單的拖拽功能,要求鼠標在小方塊上按下後可以拖着小方塊進行移動;鼠標放開後,則運動中止。react

要實現一個簡單的拖拽,須要對 mousedown, mousemove, mouseup 等多個事件進行觀察,並相應地改變小方塊的位置。es6

首先分析一下,爲了相應地移動小方塊,咱們須要知道的信息有:1). 小方塊被拖拽時的初始位置;2). 小方塊在被拖拽着移動時,須要移動到的新位置。經過 Marble Diagram 來描述一下咱們的原始流與想要獲得的流,其中最下面這個流就是咱們想要用於更新小方塊位置的流。編程

mousedown   : --d----------------------d---------
mousemove   : -m--m-m-m--m--m---m-m-------m-m-m--
mouseup     : ---------u---------------------u---

dragUpdate  : ----m-m-m-------------------m-m----

簡而言之,就是在一次 mousedownmouseup 之間觸發 mousemove 時,更新小方塊的位置。要作到這一點,最重要的操做符是 takeUntil,相關的僞代碼以下:前端工程師

mousedown.switchMap(() => mousemove.takeUntil(mouseup))

switchMaptakeUntil 加入上面的 Marble Diagram:ide

mousedown  : --d----------------------d---------
mousemove  : -m--m-m-m--m--m---m-m-------m-m-m--
mouseup    : ---------u---------------------u---
     
   stream1$ = mousedown.map(() => mousemove.takeUntil(mouseup))

stream1$   : --d----------------------d---------
                \                      \
                 m-m-m|                 -m-m|
   
   dragUpdate = stream1$.switch()

dragUpdate : ----m-m-m-------------------m-m----

其實 switchMap 就是 map + switch 組合的簡寫形式。固然,咱們還須要同時記錄一下初始位置並根據鼠標移動的距離來更新小方塊的位置,實際的實現代碼以下:

const box = document.getElementById('box')
const mouseDown$ = Rx.Observable.fromEvent(box, 'mousedown')
const mouseMove$ = Rx.Observable.fromEvent(document, 'mousemove')
const mouseUp$ = Rx.Observable.fromEvent(document, 'mouseup')

mouseDown$.map((event) => ({
  pos: getTranslate(box),
  event,
}))
.switchMap((initialState) => {
  const initialPos = initialState.pos
  const { clientX, clientY } = initialState.event
  return mouseMove$.map((moveEvent) => ({
    x: moveEvent.clientX - clientX + initialPos.x,
    y: moveEvent.clientY - clientY + initialPos.y,
  }))
  .takeUntil(mouseUp$)
})
.subscribe((pos) => {
  setTranslate(box, pos)
})

其中,getTranslatesetTranslate 主要做用就是獲取和更新小方塊的位置。具體實現能夠參見 Codepen

2. 添加初始延遲

需求:在拖拽的實際應用中,有時會但願有個初始延遲。就像手機屏幕上的諸多 App 圖標,在你想要拖拽它們進行排序時,一般須要按住圖標一小段時間,好比 200ms(以下圖所示),這時該如何操做呢?

iPhone-drag

爲了演示方便,這裏咱們先定義一個簡單的動畫,當用戶鼠標按下超過必定時間後,播放一個閃爍動畫:

.blink {
  animation: 0.4s linear blinking;
}

@keyframes blinking {
  0% { opacity: 1; }
  50% { opacity: 0; }
  100% { opacity: 1; }
}

此處咱們只作一個簡單的實現:在用戶鼠標按下時間超過 200ms 且在這 200ms 的時間內沒有發生鼠標移動時,認爲拖拽開始。僞代碼以下:

mousedown.switchMap(() => $$.delay(200).takeUntil(mousemove))

其中,上面的 $$ 指的是一個新建立的流。爲了獲得更直觀的理解,使用多個 Marble Diagram 來分段理解以前的僞代碼:

mousedown   : --d----------------------d---------
mousemove   : -m---m----m--------m-------------m-

   stream1$ = mousedown.map(() => $$.delay(200).takeUntil(mousemove))

stream1$    : --d----------------------d---------
                 \                      \
                  -|                     ----s|

   dragStart = mousedown.switchMap(() => $$.delay(200).takeUntil(mousemove))

dragStart   : -------------------------------s----

在第一次鼠標按下的 200ms 內,觸發了 mousemove 事件,因此第一次 mousedown 並無觸發一次 dragStart,而在第二次鼠標按下的 200ms 內,並無觸發 mousemove 事件,因此最後就引發了一次 dragStart

結合以前的簡單拖拽的實現,代碼以下:

mouseDown$.switchMap((event) => {
  return Rx.Observable.of({
    pos: getTranslate(box),
    event,
  })
  .delay(200)
  .takeUntil(mouseMove$)
})
.switchMap((initialState) => {
  const initialPos = initialState.pos
  const { clientX, clientY } = initialState.event
  box.classList.add('blink')
  return mouseMove$.map((moveEvent) => ({
    x: moveEvent.clientX - clientX + initialPos.x,
    y: moveEvent.clientY - clientY + initialPos.y,
  }))
  .takeUntil(mouseUp$.do(() => box.classList.remove('blink')))
})
.subscribe((pos) => {
  setTranslate(box, pos)
})

其中,多了兩句操做 #box 的 classname 的代碼,主要就是用於觸發動畫的。完整代碼見 Codepen

3. 拖拽接龍

需求:給定 n 個小方塊,要求拖拽第一個小方塊進行移動,後續的小方塊可以以間隔 0.1s 的時間跟着以前的小方塊進行延遲模仿運動。

此例中,咱們再也不要求「初始延遲」,所以針對正在拖拽着的紅色小方塊,只要沿用第一個例子中的簡單拖拽的方法,便可獲取咱們須要改變方塊位置的事件流:

mousedown.switchMap(() => mousemove.takeUntil(mouseup))

然而咱們該如何依次修改多個方塊的位置呢?首先,能夠先構造一個流來按延遲時間依次取得咱們想要改變的小方塊:

// 獲取全部小方塊,圖示的例子中給出的是 7 個小方塊
const boxes = document.getElementsByClassName('box')

// 使用 zip 操做符構造一個由 boxes 組成的流
const boxes$ = Rx.Observable.from([].slice.call(boxes, 0))
const delayBoxes$ = boxes$.zip(Rx.Observable.interval(100).startWith(0), (box) => box)

假定 7 個 boxes 在 Marble Diagram 中分別表示爲 a, b, c, d, e, f, g

boxes$          : (abcdefg)|
interval(100)   : 0---0---1---2---3---4---5---6---7---8---

   delayBoxes$ = boxes$.zip(Rx.Observable.interval(100).startWith(0), (box) => box)

delayBoxes$     : a---b---c---d---e---f---g|

只要將本來用於修改方塊位置的 mousemove 事件流 mergeMap 到上面例子中的 delayBoxes$ 上,便可完成「拖拽接龍」。僞代碼以下所示:

mousedown.switchMap(() => mousemove.takeUntil(mouseup))
  .mergeMap(() => delayBoxes$.do(() => { /* 此處更新各個小方塊的位置 */ }))

讓咱們繼續着眼於 Marble Diagram:

delayBoxes$     : ---a---b---c---d---e---f---g|
dragUpdate$     : -----m--------m----------m-------

   stream1$ = dragUpdate$.map(() => delayBoxes$)

stream1$        : -----m-------m----------m-------
                        \       \          \
                         \       \          a---b---c---d---e---f---g|
                          \       a---b---c---d---e---f---g|
                           a---b---c---d---e---f---g|

   result$ = dragUpdate$.mergeMap(() => delayBoxes$)

result$         : ---------a---b--ac--bd--cea-dfb-egc-f-d-g-e---f---g|

正如上面 Marble Diagram 所示,咱們能夠藉助流的力量從容地在合適的時機修改對應的小方塊的位置。具體的實現代碼以下所示:

const headBox = document.getElementById('head')
const boxes = document.getElementsByClassName('box')
const mouseDown$ = Rx.Observable.fromEvent(headBox, 'mousedown')
const mouseMove$ = Rx.Observable.fromEvent(document, 'mousemove')
const mouseUp$ = Rx.Observable.fromEvent(document, 'mouseup')
const delayBoxes$ = Rx.Observable.from([].slice.call(boxes, 0))
  .zip(Rx.Observable.interval(100).startWith(0), (box) => box)

mouseDown$.map((e) => {
  const pos = getTranslate(headBox)
  return {
    pos,
    event: e,
  }
})
.switchMap((initialState) => {
  const initialPos = initialState.pos
  const { clientX, clientY } = initialState.event
  return mouseMove$.map((moveEvent) => ({
    x: moveEvent.clientX - clientX + initialPos.x,
    y: moveEvent.clientY - clientY + initialPos.y,
  }))
  .takeUntil(mouseUp$)
})
.mergeMap((pos) => {
  return delayBoxes$.do((box) => {
    setTranslate(box, pos)
  })
})
.subscribe()

完整的實現代碼見 Codepen

小結

  • 這篇文章介紹了關於拖拽的三個實際場景:

    • 在簡單拖拽的實例中,使用到了 takeUntil, switchMap 操做符;

    • 須要添加初始延遲時,咱們額外使用到 delay 操做符;

    • 在最後的拖拽接龍實例中,mergeMap 操做符和 zip + interval 的組合發揮了很大的做用

  • 相信看完本文之後,大家可以深入體會到:結合 Marble Diagram 來理解 RxJS 的流是一個很是棒的方法!

最後你們能夠思考一下:在第三個例子中,若是把 mergeMap 改成 switchMap 或者 concatMap 會發生什麼?這是課後做業。下課!

相關文章
相關標籤/搜索