30 天精通 RxJS (04): 什么是 Observable ?

这是转载【30天精通 RxJS】的 04 篇,如果还没看过 03 篇可以往这边走:
30 天精通 RxJS (03): Functional Programming 通用函式

要理解 Observable 之前,我们必须先谈谈两个设计模式(Design Pattern), Iterator Pattern 跟 Observer Pattern。今天这篇文章会带大家快速的了解这两个设计模式,并解释这两个 Pattern 跟 Observable 之间的关系!

Observer Pattern

Observer Pattern 其实很常遇到,在许多 API 的设计上都用了 Observer Pattern 实作,最简单的例子就是 DOM 物件的事件监听,程式码如下

function clickHandler(event) {
    console.log('user click!');
}

document.body.addEventListener('click', clickHandler)

在上面的程式码,我们先宣告了一个 clickHandler 函式,再用 DOM 物件 (范例是 body) 的 addEventListener 来监听点击(click)事件,每次使用者在 body 点击滑鼠就会执行一次 clickHandler,并把相关的资讯(event)带进来!这就是观察者模式,我们可以对某件事注册监听,并在事件发生时,自动执行我们注册的监听者(listener)。

Observer 的观念其实就这麽的简单,但笔者希望能透过程式码带大家了解,如何实作这样的 Pattern!

首先我们需要一个建构式,这个建构式 new 出来的实例可以被监听。

这裡我们先用 ES5 的写法,会再附上 ES6 的写法

function Producer() {

    // 这个 if 只是避免使用者不小心把 Producer 当作函式来调用
    if(!(this instanceof Producer)) {
      throw new Error('请用 new Producer()!');
      // 仿 ES6 行为可用: throw new Error('Class constructor Producer cannot be invoked without 'new'')
    }

    this.listeners = [];
}

// 加入监听的方法
Producer.prototype.addListener = function(listener) {
    if(typeof listener === 'function') {
        this.listeners.push(listener)
    } else {
        throw new Error('listener 必须是 function')
    }
}

// 移除监听的方法
Producer.prototype.removeListener = function(listener) {
    this.listeners.splice(this.listeners.indexOf(listener), 1)
}

// 发送通知的方法
Producer.prototype.notify = function(message) {
    this.listeners.forEach(listener => {
        listener(message);
    })
}

这裡用到了 this, prototype 等观念,大家不了解可以去看我的一支影片专门讲解这几个观念!

附上 ES6 版本的程式码,跟上面程式码的行为基本上是一样的

class Producer {
    constructor() {
        this.listeners = [];
    }
    addListener(listener) {
        if(typeof listener === 'function') {
            this.listeners.push(listener)
        } else {
            throw new Error('listener 必须是 function')
        }
    }
    removeListener(listener) {
        this.listeners.splice(this.listeners.indexOf(listener), 1)
    }
    notify(message) {
        this.listeners.forEach(listener => {
            listener(message);
        })
    }
}

有了上面的程式码后,我们就可以来建立物件实例了

var egghead = new Producer(); 
// new 出一个 Producer 实例叫 egghead

function listener1(message) {
    console.log(message + 'from listener1');
}

function listener2(message) {
    console.log(message + 'from listener2');
}

egghead.addListener(listener1); // 注册监听
egghead.addListener(listener2);

egghead.notify('A new course!!') // 当某件事情方法时,执行

当我们执行到这裡时,会印出:

a new course!! from listener1
a new course!! from listener2

每当 egghead.notify 执行一次,listener1listener2 就会被通知,而这些 listener 可以额外被添加,也可以被移除!

虽然我们的实作很简单,但它很好的说明了 Observer Pattern 如何在事件(event)跟监听者(listener)的互动中做到去藕合(decoupling)。

Iterator Pattern

Iterator 是一个物件,它的就像是一个指针(pointer),指向一个资料结构并产生一个序列(sequence),这个序列会有资料结构中的所有元素(element)。

先让我们来看看原生的 JS 要怎麽建立 iterator

var arr = [1, 2, 3];

var iterator = arr[Symbol.iterator]();

iterator.next();
// { value: 1, done: false }
iterator.next();
// { value: 2, done: false }
iterator.next();
// { value: 3, done: false }
iterator.next();
// { value: undefined, done: true }

JavaScript 到了 ES6 才有原生的 Iterator

在 ECMAScript 中 Iterator 最早其实是要採用类似 Python 的 Iterator 规范,就是 Iterator 在没有元素之后,执行 next 会直接抛出错误;但后来经过一段时间讨论后,决定採更 functional 的做法,改成在取得最后一个元素之后执行 next 永远都回传 { done: true, value: undefined }

JavaScript 的 Iterator 只有一个 next 方法,这个 next 方法只会回传这两种结果:

  1. 在最后一个元素前: { done: false, value: elem }
  2. 在最后一个元素之后: { done: true, value: undefined }

当然我们可以自己实作简单的 Iterator Pattern

function IteratorFromArray(arr) {
    if(!(this instanceof IteratorFromArray)) {
        throw new Error('请用 new IteratorFromArray()!');
    }
    this._array = arr;
    this._cursor = 0;   
}

IteratorFromArray.prototype.next = function() {
    return this._cursor < this._array.length ?
        { value: this._array[this._cursor++], done: false } :
        { done: true };
}

附上 ES6 版本的程式码,行为同上

class IteratorFromArray {
    constructor(arr) {
        this._array = arr;
        this._cursor = 0;
    }

    next() {
        return this._cursor < this._array.length ?
        { value: this._array[this._cursor++], done: false } :
        { done: true };
    }
}

Iterator Pattern 虽然很单纯,但同时带来了两个优势,第一它渐进式取得资料的特性可以拿来做延迟运算(Lazy evaluation),让我们能用它来处理大资料结构。第二因为 iterator 本身是序列,所以可以实作所有阵列的运算方法像 map, filter... 等!

这裡我们利用最后一段程式码实作 map 试试

class IteratorFromArray {
    constructor(arr) {
        this._array = arr;
        this._cursor = 0;
    }

    next() {
        return this._cursor < this._array.length ?
        { value: this._array[this._cursor++], done: false } :
        { done: true };
    }

    map(callback) {
        const iterator = new IteratorFromArray(this._array);
        return {
            next: () => {
                const { done, value } = iterator.next();
                return {
                    done: done,
                    value: done ? undefined : callback(value)
                }
            }
        }
    }
}

var iterator = new IteratorFromArray([1,2,3]);
var newIterator = iterator.map(value => value + 3);

newIterator.next();
// { value: 4, done: false }
newIterator.next();
// { value: 5, done: false }
newIterator.next();
// { value: 6, done: false }

补充: 延迟运算(Lazy evaluation)

延迟运算,或说 call-by-need,是一种运算策略(evaluation strategy),简单来说我们延迟一个表达式的运算时机直到真正需要它的值在做运算。

以下我们用 generator 实作 iterator 来举一个例子

    function* getNumbers(words) {
        for (let word of words) {
            if (/^[0-9]+$/.test(word)) {
                yield parseInt(word, 10);
            }
        }
    }

    const iterator = getNumbers('30 天精通 RxJS (04)');

    iterator.next();
    // { value: 3, done: false }
    iterator.next();
    // { value: 0, done: false }
    iterator.next();
    // { value: 0, done: false }
    iterator.next();
    // { value: 4, done: false }
    iterator.next();
    // { value: undefined, done: true }

这裡我们写了一个函式用来抓取字串中的数字,在这个函式中我们用 for...of 的方式来取得每个字元并用正则表示式来判断是不是数值,如果为真就转成数值并回传。当我们把一个字串丢进 getNumbers 函式时,并没有马上运算出字串中的所有数字,必须等到我们执行 next() 时,才会真的做运算,这就是所谓的延迟运算(evaluation strategy)

Observable

在了解 Observer 跟 Iterator 后,不知道大家有没有发现其实 Observer 跟 Iterator 有个共通的特性,就是他们都是 渐进式(progressive) 的取得资料,差别只在于 Observer 是生产者(Producer)推送资料(push),而 Iterator 是消费者(Consumer)要求资料(pull)!

image.png

Observable 其实就是这两个 Pattern 思想的结合,Observable 具备生产者推送资料的特性,同时能像序列,拥有序列处理资料的方法(map, filter...)!

更简单的来说,Observable 就像是一个序列,裡面的元素会随著时间推送

注意这裡讲的是 思想的结合,Observable 跟 Observer 在实作上还是有差异,这我们在下一篇文章中讲到。

今日小结

今天讲了 Iterator 跟 Observer 两个 Pattern,这两个 Pattern 都是渐进式的取得元素,差异在于 Observer 是靠生产者推送资料,Iterator 则是消费者去要求资料,而 Observable 就是这两个思想的结合!

今天的观念需要比较多的思考,希望读者能多花点耐心想一想,如果有任何问题请在下方留言给我。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 介绍 RxJS是一个异步编程的库,同时它通过observable序列来实现基于事件的编程。它提供了一个核心的类型:...
    泓荥阅读 16,505评论 0 12
  • 本篇文章介主要绍RxJava中操作符是以函数作为基本单位,与响应式编程作为结合使用的,对什么是操作、操作符都有哪些...
    嘎啦果安卓兽阅读 2,782评论 0 10
  • “羊有跪乳之恩,鸦有反哺之义” 01 今晚回家回得早,妈妈洗过澡准备睡觉,我突然间想起有件很久就想做,而一直没有做...
    Coco赖阅读 364评论 4 2
  • 中午去参加一个宴会,跟同事英子姐坐位相临,平日里大家联系也不多,可近距离接触,却不禁让我刮目相看。 原因是,她的儿...
    安然ZCR阅读 988评论 6 3