资讯专栏INFORMATION COLUMN

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

megatron / 3140人阅读

摘要:观察者模式,是设计模式之一。即便如此,笔者仍精心准备了这篇博客,期望用最简单的方式来介绍下该模式。这里有个小知识点提一下函数对象的属性就是该函数名最后是思路是通过找到指定数组,然后对数组中的回调函数进行依次调用,达到发布的目的。

观察者模式,是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) {}
}

简单解释下,

registration就是上面提到的注册表,至于为什么把它设计成一个对象是因为考虑到xxx公司可能有更多类型的邮件,比如 游戏,金融,投资理财等等,所以就把它设计成对象以key-value的形式存储订阅者, 比如:{"game":[],"monetary":[]}该形式

subscribe 则是publisher提供给我们的对其进行订阅的方法,参数是typefn。type就是邮件的类型,fn就是我们提供给publisher用于通知我的渠道 (邮箱)。在JavaScript中更多的是回调函数

unSubscribepublisher提供给我们的对其进行取消订阅的方法,参数是typefnName。type就不多说了,fnName则是我们提供给publisher用于取消订阅的标志,比如说邮箱,或者是回调函数的名字等等。

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季中赛冠军!")

运行看下结果:

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

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

再运行看下结果:

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

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

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95743.html

相关文章

  • 听飞狐JavaScript设计模式系列03

    摘要:闭包与柯里化闭包有权访问另一个函数作用域中变量的函数。柯里化把接受多个参数的函数变换成接受一个单一参数最初函数的第一个参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 本回内容介绍 上一回聊到JS的Object类型,简单模拟了一下Java的Map,介一讲,偶们来聊一下函数好唔好,介可系JS世界的一等公民哟。从函数开始,我们就将逐步过渡到设计模式,来吧,帅狐带你装逼带你飞:...

    levy9527 评论0 收藏0
  • 听飞狐JavaScript设计模式系列04

    摘要:介一回,偶们来聊一下用中的类,有些盆友可能用过或者的,知道语法糖,可是在中并没有,中需要用到构造函数来模拟类。而且要注意一点,构造函数没有语句,是自动返回。 本回内容介绍 上一回聊到JS的Function类型,做了柯里化,数组去重,排序的题。 介一回,偶们来聊一下用JS中的类,有些盆友可能用过ES6或者TypeScript的,知道Class语法糖,可是在ES5中并没有,ES5中需要用到...

    kgbook 评论0 收藏0
  • JavaScript - 收藏集 - 掘金

    摘要:插件开发前端掘金作者原文地址译者插件是为应用添加全局功能的一种强大而且简单的方式。提供了与使用掌控异步前端掘金教你使用在行代码内优雅的实现文件分片断点续传。 Vue.js 插件开发 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins译者:jeneser Vue.js插件是为应用添加全局功能的一种强大而且简单的方式。插....

    izhuhaodev 评论0 收藏0
  • 听飞狐JavaScript设计模式系列08

    摘要:本回内容介绍上一回聊到工厂模式,略抽象。官方说法,门面模式是指提供一个统一的接口去访问多个子系统的多个不同的接口,为子系统中的一组接口提供一个统一的高层接口。使得子系统更容易使用。 本回内容介绍 上一回聊到工厂模式,略抽象。介一回,咱聊门面模式就比较容易了,门面模式也叫外观模式(facade)。官方说法,门面模式是指提供一个统一的接口去访问多个子系统的多个不同的接口,为子系统中的一组接...

    saucxs 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<