30 天精通 RxJS (08):简易拖拉实作 - take, first, takeUntil, concatAll

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

Operators

take

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

JSBin | JSFiddle

这裡可以看到我们的 source 原本是会发出无限个元素的,但这裡我们用 take(3) 就会只取前 3 个元素,取完后就直接结束(complete)。

用 Marble diagram 表示如下

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

first

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

JSBin | JSFiddle

用 Marble diagram 表示如下

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

takeUntil

在实务上 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了

JSBin | JSFiddle

这裡我们一开始先用 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)。

concatAll

有时我们的 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'); }
});

JSBin | JSFiddle

这个范例我们每点击一次 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

JSBin | JSFiddle

这裡可以看到 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 = 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,谢谢。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容