用最简单的方式聊一下JavaScript中的观察者模式

观察者模式,是JavaScript设计模式之一。当然也不仅仅限于JavaScript这门语言,网上对该模式的介绍已是多如牛毛,而且讲得各有特色各有心得。即便如此,笔者仍精心准备了这篇博客,期望用最简单的方式来介绍下该模式。

首先来看下维基百科对 观察者模式 的解释:

观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

其实笔者更倾向于它的另一个名字发布/订阅模式(Publish/Subscribe),因为更能表达出该模式的核心思路,那就是:发布订阅两个过程。是不是还感觉模棱两可?不用担心,下面就用咱们身边发生的事情来做个形象化的解释:
大家都有订阅网站邮件的经历吧?如果你没有的话,emmmmm....那就继续往下看吧哈哈!!
假如我今天想订阅xxx公司的邮件,那么这里就涉及到两个对象:xxx公司
从行为上来看就是我订阅了xxx公司邮件,xxx公司会发送邮件给我的邮箱。但某天我不想再收到xxx公司的邮件了,那么我可以取消订阅,这样xxx公司就不会再发邮件到我的邮箱。

说到这里,是不是就有点眉头了呢?好,我们继续往下说,
通过刚刚的形象化解释,我们可以罗列下观察者模式的一些核心的东西:

对象:我 (订阅者), xxx公司 (发布者), 可以直接对应 发布/订阅模式(Publish/Subscribe)
行为: 订阅发送取消订阅

说不如做,下面开始用代码来更直观的描绘下观察者模式吧。
首先我们定义一个发布者 (相当于xxx公司)

let publisher = {
    
}

那么一起来按照订阅邮件的过程想象下,发布者具有那些属性或者方法?

  • 首先,我们订阅一个xxx网站的邮件,是不是需要xxx网站给我们提供订阅入口?那么publisher中必定会有一个方法提供给我们实现订阅
  • 其次,如果xxx公司要向订阅者们发送自己的邮件,是不是需要一个方法去做?那么publisher中必定会有一个方法提供给我们实现发送或者说说发布
  • 再然后,如例子所说如果我突然不想订阅xxx公司的邮件了,xxx公司 就得提供给我一个取消订阅的入口,那么publisher中必定会有一个方法提供给我们实现取消订阅
  • 最后,如果我们订阅了xxx公司的邮件,那么他就得记录我订阅所用的邮箱地址吧,所以publisher中必定会有一个“注册表”来存储订阅的对象,也就是说我们的 邮箱地址

说到这里一切都了然了,下面还是讲想象到的东西用代码表达出来吧

let publisher = {
    registration: {},
    subscribe: function (type, fn) {},
    unSubscribe: function (type, fnName) {},
    publish: function (type, message) {}
}

简单解释下,

  1. registration就是上面提到的注册表,至于为什么把它设计成一个对象是因为考虑到xxx公司可能有更多类型的邮件,比如 游戏,金融,投资理财等等,所以就把它设计成对象以key-value的形式存储订阅者, 比如:{'game':[],'monetary':[]}该形式
  2. subscribe 则是publisher提供给我们的对其进行订阅的方法,参数是typefn。type就是邮件的类型,fn就是我们提供给publisher用于通知我的渠道 (邮箱)。在JavaScript中更多的是回调函数
  3. unSubscribepublisher提供给我们的对其进行取消订阅的方法,参数是typefnName。type就不多说了,fnName则是我们提供给publisher用于取消订阅的标志,比如说邮箱,或者是回调函数的名字等等。
  4. publish说到比较重要的方法,这就是publisher向所有订阅者发布消息的方法。

下面开始一步一步得实现三个方法,registration保持不变:

首先是 subscribe
subscribe: function (type, fn) {
    if (Object.keys(this.registration).indexOf(type) >= 0) {
        this.registration[type].push(fn);
    } else {
        this.registration[type] = [];
        this.registration[type].push(fn);
    }
}

这里的思路是将 Callback Function 存储到registration对于类型的数组中,以待publish调用。

然后是 unSubscribe
unSubscribe: function (type, fnName) {
    if (Object.keys(this.registration).indexOf(type) >= 0) {
        let index = -1;
        this.registration[type].forEach(function (func, idx) {
            if (func.name === fnName) {
                index = idx;
            }
        })
        index > -1 ? this.registration[type].splice(index, 1) : null
    }
}

思路是首先通过 type 确定数组对象,然后通过方法对象的名字进行判断,最后直接剔除操作。
** 这里有个小知识点提一下:函数对象的name属性就是该函数名 **

最后是 publish
publish: function (type, message) {
    if (Object.keys(this.registration).indexOf(type) >= 0) {
        for (let fn of this.registration[type]) {
            fn(message)
        }
    }
}

思路是通过 type 找到指定数组,然后对数组中的回调函数进行依次调用,达到发布的目的。

写到这里,发布者Publisher已经完成。那么下面开始写订阅者Subscriber,如上面所说其实订阅者就是一个 回调函数,例如:

let subscriber = function (param) {
    //do something
}

所以下面将整个代码展示并演示下效果:

let publisher = {
    registration: {},
    subscribe: function (type, fn) {
        if (Object.keys(this.registration).indexOf(type) >= 0) {
            this.registration[type].push(fn);
        } else {
            this.registration[type] = [];
            this.registration[type].push(fn);
        }
    },
    unSubscribe: function (type, fnName) {
        if (Object.keys(this.registration).indexOf(type) >= 0) {
            let index = -1;
            this.registration[type].forEach(function (func, idx) {
                if (func.name === fnName) {
                    index = idx;
                }
            })
            index > -1 ? this.registration[type].splice(index, 1) : null
        }
    },
    publish: function (type, message) {
        if (Object.keys(this.registration).indexOf(type) >= 0) {
            for (let fn of this.registration[type]) {
                fn(message)
            }
        }
    }
}

let subscriberA = function (message) {
    console.log(`A收到通知:${message}`)
};

let subscriberB = function (message) {
    console.log(`B收到通知:${message}`)
};

let subscriberC = function (message) {
    console.log(`C收到通知:${message}`)
};

publisher.subscribe('game', subscriberA);
publisher.subscribe('game', subscriberB);
publisher.subscribe('game', subscriberC);

publisher.publish('game', '恭喜RNG获得LOL 2018季中赛冠军!')

运行看下结果:

用最简单的方式聊一下JavaScript中的观察者模式_第1张图片

结果如想象中一样。
那再试一下取消订阅,在 publish 之前加一段

publisher.unSubscribe('game', subscriberB.name)

再运行看下结果:

用最简单的方式聊一下JavaScript中的观察者模式_第2张图片

我们已经看到 订阅者B 在取消订阅后就没再收到任何消息。

其实观察者模式能做的东西还有很多,比如事件的监听、状态发生变化时的广播等等。已经有过接触的朋友都可能意识到这个模式特别灵活,在两个角色之间正常通信的同时也尽可能得实现了解耦,给开发带来极大的便利。其中有名的 Knockout 的核心之一就是观察者模式,所以说观察者模式在前端开发中起到了举足轻重的作用。

源码 在这,有兴趣的朋友可以看下

好了,写到这里本篇博客就结束了。有问题的朋友可以在下方讨论;如果文章有不足或者错误的地方,烦请大家多多指正。Thanks !!!

你可能感兴趣的