iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 14
6
Modern Web

30 天精通 RxJS系列 第 14

30 天精通 RxJS (13): Observable Operator - delay, delayWhen

30 天精通 RxJS (13): Observable Operator - delay, delayWhen

在所有非同步中行為中,最麻煩的大概就是 UI 操作了,因為 UI 是直接影響使用者的感受,如果處理的不好對使用體驗會大大的扣分!

UI 大概是所有非同步行為中最不好處理的,不只是因為它直接影響了用戶體驗,更大的問題是 UI 互動常常是高頻率觸發的事件,而且多個元件間的時間序需要不一致,要做到這樣的 UI 互動就不太可能用 Promise 或 async/await,但是用 RxJS 仍然能輕易地處理!

今天我們要介紹的兩個 Operators,delay 跟 delayWhen 都是跟 UI 互動比較相關的。當我們的網頁越來越像應用程式時,UI 互動就變得越重要,讓我們來試試如何用 RxJS 完成基本的 UI 互動!

Operators

delay

delay 可以延遲 observable 一開始發送元素的時間點,範例如下

var source = Rx.Observable.interval(300).take(5);

var example = source.delay(500);

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// 4

JSBin | JSFiddle

當然直接從 log 出的訊息看,是完全看不出差異的

讓我們直接看 Marble Diagram

source : --0--1--2--3--4|
        delay(500)
example: -------0--1--2--3--4|

從 Marble Diagram 可以看得出來,第一次送出元素的時間變慢了,雖然在這裡看起來沒什麼用,但是在 UI 操作上是非常有用的,這個部分我們最後示範。

delay 除了可以傳入毫秒以外,也可以傳入 Date 型別的資料,如下使用方式

var source = Rx.Observable.interval(300).take(5);

var example = source.delay(new Date(new Date().getTime() + 1000));

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});

JSBin | JSFiddle

這好像也能用在預定某個日期,讓程式掛掉

delayWhen

delayWhen 的作用跟 delay 很像,最大的差別是 delayWhen 可以影響每個元素,而且需要傳一個 callback 並回傳一個 observable,範例如下

var source = Rx.Observable.interval(300).take(5);

var example = source
              .delayWhen(
                  x => Rx.Observable.empty().delay(100 * x * x)
              );

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});

JSBin | JSFiddle

這時我們的 Marble Diagram 如下

source : --0--1--2--3--4|
    .delayWhen(x => Rx.Observable.empty().delay(100 * x * x));
example: --0---1----2-----3-----4|

這裡傳進來的 x 就是 source 送出的每個元素,這樣我們就能對每一個做延遲。

這裡我們用 delay 來做一個小功能,這個功能很簡單就是讓多張照片跟著滑鼠跑,但每張照片不能跑一樣快!

首先我們準備六張大頭照,並且寫進 HTML

<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover6.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover5.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover4.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover3.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover2.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover1.jpg" alt="">

用 CSS 把 img 改成圓形,並加上邊筐以及絕對位置

img{
  position: absolute;
  border-radius: 50%;
  border: 3px white solid;
  transform: translate3d(0,0,0);
}

再來寫 JS,一樣第一步先抓 DOM

var imgList = document.getElementsByTagName('img');

第二步建立 observable

var movePos = Rx.Observable.fromEvent(document, 'mousemove')
.map(e => ({ x: e.clientX, y: e.clientY }))

第三步撰寫邏輯

function followMouse(DOMArr) {
  const delayTime = 600;
  DOMArr.forEach((item, index) => {
    movePos
      .delay(delayTime * (Math.pow(0.65, index) + Math.cos(index / 4)) / 2)
      .subscribe(function (pos){
        item.style.transform = 'translate3d(' + pos.x + 'px, ' + pos.y + 'px, 0)';
      });
  });
}

followMouse(Array.from(imgList))

這裡我們把 imgList 從 Collection 轉成 Array 後傳入 followMouse(),並用 forEach 把每個 omg 取出並利用 index 來達到不同的 delay 時間,這個 delay 時間的邏輯大家可以自己想,不用跟我一樣,最後 subscribe 就完成啦!

最後完整的範例在這裡

今日小結

今天我們介紹了兩個 operators 並帶了一個小範例,這兩個 operators 在 UI 操作上都非常的實用,我們明天會接著講幾個 operators 可以用來做高頻率觸發的事件優化!


上一篇
30 天精通 RxJS (12): Observable Operator - scan, buffer
下一篇
30 天精通 RxJS (14): Observable Operator - throttle, debounce
系列文
30 天精通 RxJS30

2 則留言

1
feng619
iT邦新手 5 級 ‧ 2016-12-31 07:25:56

範例很有趣XDD

1
gaozhi
iT邦新手 5 級 ‧ 2017-01-03 15:49:18

請問delayWhen的範例,
好像無法增加每一個元素的延遲時間,
我將source的take增加到100,還是感受不出來有增加延遲,
不知道是不是我有理解錯誤?

應該是我文章寫得不好,take 增加是不影響的。
delayWhen 簡單說就是等到 callback 回傳的 observable 送出元素後,真正的 observable 才開始送出元素

舉例來說

var source = Rx.Observable.interval(300).take(5);
var someObs = Rx.Observable.interval(1000).take(1);
var example = source
              .delayWhen(x => someObs);

JSBin

上面這個例子,delayWhen 的 callback 會被執行了 5 次,因為 source 會送出五個元素,也就是說會回傳五次 someObs,而第一次回傳的 someObs 送出第一個元素時,source 送出的第一個元素就會被送出,當第二次回傳的 someObs 送出第一個元素時,source 送出的第二個元素就會被送出...以此類推

也就是說 someObs 送出幾個元素其實跟延遲時間是無關的,重點是第 n 個回傳的 someObs 送出第一個元素的時間

以這個例子來說 source 每個元素都延遲了 1 秒(1000ms),因為每個元素都延遲了一秒,所以每個元素間的時間差仍然是 300 毫秒。

我們可以用 callback 傳入的 x 來改變每個元素被延遲的時間差,才會真的導致元素送出的時間差改變,例如下面這個例子

var source = Rx.Observable.interval(300).take(5);

var example = source
              .delayWhen(
                  x => Rx.Observable.interval(100 * x * x)
              );

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});

JSBin

這個例子就是
第一個元素被延遲了 100ms (100 * 1 * 1)
第二個元素被延遲了 400ms (100 * 2 * 2)
第三個元素被延遲了 900ms (100 * 3 * 3)
第四個元素被延遲了 1600ms (100 * 4 * 4)
第五個元素被延遲了 2500ms (100 * 5 * 5)

但請記得這裡的延遲是總時間的延遲,而不是每個元素的時間間隔

也就是說每個元素的時間間隔變成

第幾個到第幾個 時間差 計算式
0~1 400ms (300+100)
1~2 600ms (300+400-100)
2~3 800ms (300+900-400)
3~4 1000ms (300+1600-900)
4~5 1200ms (300+2500-1600)

第一個 300 是 source 的元素原本的間隔時間,後面加上自己延遲的時間,要再扣掉上一個元素被延遲的時間,才是真的元素間的時間間隔。

感謝你,我會再找時間改一下這篇文章,讓文章更清楚一點

gaozhi iT邦新手 5 級‧ 2017-01-03 18:13:59 檢舉

謝謝解答,感謝您分享這系列好文喔。

我要留言

立即登入留言