[转载升级V7]30 天精通 RxJS (08): 简易拖拉实践-take, first,takeUntil,concatAll

118 阅读5分钟
如果是你会如何实作拖拉的功能?

我们今天要接着讲take, first, takeUntil, concatAll 这四个operators,并且实作一个简易的拖拉功能。

Operators

take

take 是一个很简单的operator,顾名思义就是取前几个元素后就结束,范例如下

const source = interval(1000);
const 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

first 会取observable 送出的第1 个元素之后就直接结束,行为跟take(1) 一致。

const source = interval(1000);
const 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

在实践上takeUntil 很常使用到,他可以在某件事情发生时,让一个observable 直接发出完成(complete)信息,范例如下

const source = interval(1000);
const click = fromEvent(document.body, 'click');
const 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。(因此source, click, example都是observalbe)

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)。

concatAll

有时我们的Observable 送出的元素又是一个observable,就像是二维数组,数组里面的元素是数组,这时我们就可以用把它摊平成concatAll一维数组,大家也可以直接把concatAll 想成把所有元素concat 起来。

const click = fromEvent(document.body,'click');
const source = click.map(e => of(1,2,3));
const 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)------------

这里可以看到sourceobservable 内部每次发送的值也是observable,这时我们用concatAll 就可以把source 摊平成example。

这里需要注意的是concatAll会处理source 先发出来的observable,必须等到这个observable 结束,才会再处理下一个source 发出来的observable,让我们用下面这个范例说明。

const obs1 = interval(1000).take(5);
const obs2 = interval(500).take(2);
const obs3 = interval(2000).take(1);
const source = of(obs1, obs2, obs3);
const 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 后,我们就很轻松地做出拖拉的功能,先让我们来看一下需求

  1. 首先画面上有一个元素(#drag)
  2. 当滑鼠在元素(#drag)上按下左键(mousedown)时,开始监听滑鼠移动(mousemove)的位置
  3. 当滑鼠左键放掉(mouseup)时,结束监听滑鼠移动
  4. 当滑鼠移动(mousemove)被监听时,跟着修改元素的样式属性

第一步我已经完成了,大家可以直接到以下两个连结做练习

第二步我们要先取得各个DOM 物件,元素(#drag) 跟body。

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

要取得body 的原因是因为滑鼠移动(mousemove)跟滑鼠左键放掉(mouseup)都应该是在整个body 监听。

第三步我们写出各个会用到的监听事件,并用fromEvent来取得各个observable。

  • 对#drag 监听mousedown
  • 对body 监听mouseup
  • 对body 监听mousemove
const mouseDown = fromEvent(dragDOM, 'mousedown');
const mouseUp = fromEvent(body, 'mouseup');
const mouseMove = 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,并且完成了一个简易的拖拉功能,我们之后会把这个拖拉功能做得更完整,并且整合其他功能!