30 天精通 RxJS (12): Observable Operator - scan, buffer

今天要继续讲两个简单的 transformation operators 并带一些小范例,这两个 operators 都是实务上很常会用到的方法。

Operators

scan

scan 其实就是 Observable 版本的 reduce 只是命名不同。如果熟悉阵列操作的话,应该会知道原生的 JS Array 就有 reduce 的方法,使用方式如下

var arr = [1, 2, 3, 4];
var result = arr.reduce((origin, next) => { 
    console.log(origin)
    return origin + next
}, 0);

console.log(result)
// 0
// 1
// 3
// 6
// 10

JSBin | JSFiddle

reduce 方法需要传两个参数,第一个是 callback 第二个则是起始状态,这个 callback 执行时,会传入两个参数一个是原本的状态,第二个是修改原本状态的参数,最后回传一个新的状态,再继续执行。

所以这段程式码是这样执行的

  • 第一次执行 callback 起始状态是 0 所以 origin 传入 0,next 为 arr 的第一个元素 1,相加之后变成 1 回传并当作下一次的状态。
  • 第二次执行 callback,这时原本的状态(origin)就变成了 1,next 为 arr 的第二个元素 2,相加之后变成 3 回传并当作下一次的状态。
  • 第三次执行 callback,这时原本的状态(origin)就变成了 3,next 为 arr 的第三个元素 3,相加之后变成 6 回传并当作下一次的状态。
  • 第三次执行 callback,这时原本的状态(origin)就变成了 6,next 为 arr 的第四个元素 4,相加之后变成 10 回传并当作下一次的状态。
  • 这时 arr 的元素都已经遍历过了,所以不会直接把 10 回传。

scan 整体的运作方式都跟 reduce 一样,范例如下

var source = Rx.Observable.from('hello')
             .zip(Rx.Observable.interval(600), (x, y) => x);

var example = source.scan((origin, next) => origin + next, '');

example.subscribe({
    next: (value) => { console.log(value); },
    error: (err) => { console.log('Error: ' + err); },
    complete: () => { console.log('complete'); }
});
// h
// he
// hel
// hell
// hello
// complete

JSBin |JSFiddle

画成 Marble Diagram

source : ----h----e----l----l----o|
    scan((origin, next) => origin + next, '')
example: ----h----(he)----(hel)----(hell)----(hello)|

这裡可以看到第一次传入 'h''' 相加,返回 'h' 当作下一次的初始状态,一直重複下去。

scan 跟 reduce 最大的差别就在 scan 一定会回传一个 observable 实例,而 reduce 最后回传的值有可能是任何资料型别,必须看使用者传入的 callback 才能决定 reduce 最后的返回值。

Jafar Husain 就曾说:「JavaScript 的 reduce 是错了,它最后应该永远回传阵列才对!」

如果大家之前有到这裡练习的话,会发现 reduce 被设计成一定回传阵列,而这个网页就是 Jafar 做的。

scan 很常用在状态的计算处理,最简单的就是对一个数字的加减,我们可以绑定一个 button 的 click 事件,并用 map 把 click event 转成 1,之后送处 scan 计算值再做显示。

这裡笔者写了一个小范例,来示范如何做最简单的加减

const addButton = document.getElementById('addButton');
const minusButton = document.getElementById('minusButton');
const state = document.getElementById('state');

const addClick = Rx.Observable.fromEvent(addButton, 'click').mapTo(1);
const minusClick = Rx.Observable.fromEvent(minusButton, 'click').mapTo(-1);

const numberState = Rx.Observable.empty()
  .startWith(0)
  .merge(addClick, minusClick)
  .scan((origin, next) => origin + next, 0)

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

JSBin | JSFiddle

这裡我们用了两个 button,一个是 add 按钮,一个是 minus 按钮。

我把这两个按钮的点击事件各建立了 addClcik, minusClick 两个 observable,这两个 observable 直接 mapTo(1) 跟 mapTo(-1),代表被点击后会各自送出的数字!

接著我们用了 empty() 建立一个空的 observable 代表画面上数字的状态,搭配 startWith(0) 来设定初始值,接著用 merge 把两个 observable 合併透过 scan 处理之后的逻辑,最后在 subscribe 来更改画面的显示。

这个小范例用到了我们这几天讲的 operators,包含 mapTo, empty, startWith, merge 还有现在讲的 scan,建议读者一定要花时间稍微练习一下。

buffer

buffer 是一整个家族,总共有五个相关的 operators

  • buffer
  • bufferCount
  • bufferTime
  • bufferToggle
  • bufferWhen

这裡比较常用到的是 buffer, bufferCount 跟 bufferTime 这三个,我们直接来看范例。

var source = Rx.Observable.interval(300);
var source2 = Rx.Observable.interval(1000);
var example = source.buffer(source2);

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

JSBin | JSFiddle

画成 Marble Diagram 则像是

source : --0--1--2--3--4--5--6--7..
source2: ---------0---------1--------...
            buffer(source2)
example: ---------([0,1,2])---------([3,4,5])    

buffer 要传入一个 observable(source2),它会把原本的 observable (source)送出的元素缓存在阵列中,等到传入的 observable(source2) 送出元素时,就会触发把缓存的元素送出。

这裡的范例 source2 是每一秒就会送出一个元素,我们可以改用 bufferTime 简洁的表达,如下

var source = Rx.Observable.interval(300);
var example = source.bufferTime(1000);

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

JSBin | JSFiddle

除了用时间来作缓存外,我们更常用数量来做缓存,范例如下

var source = Rx.Observable.interval(300);
var example = source.bufferCount(3);

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

JSBin | JSFiddle
在实务上,我们可以用 buffer 来做某个事件的过滤,例如像是滑鼠连点才能真的执行,这裡我们一样写了一个小范例

const button = document.getElementById('demo');
const click = Rx.Observable.fromEvent(button, 'click')
const example = click
                .bufferTime(500)
                .filter(arr => arr.length >= 2);

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

JSBin | JSFiddle

这裡我们只有在 500 毫秒内连点两下,才能成功印出 'success',这个功能在某些特殊的需求中非常的好用,也能用在批次处理来降低 request 传送的次数!

今日小结

今天我们介绍了两个 operators 分别是 scan, buffer,也做了两个小范例,不知道读者有没有收穫呢?

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

推荐阅读更多精彩内容