微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

谈谈 JavaScript 的观察者模式(自定义事件)

2016-08-28 20:37 伯乐专栏\/winty

(点击上方公众号,可快速关注)

作者:伯乐在线专栏作者 - winty

链接:http://web.jobbole.com/87287/

点击 → 了解如何加入专栏作者

呼呼,前不久参加了一个笔试,里面有一到JS编程题,当时看着题目就蒙圈。后来研究了一下,原来就是所谓的观察者模式。就记下来 ^_^


题目


[附加题] 请实现下面的自定义事件 Event 对象的接口,功能见注释(测试1)

Event 对象的接口需要能被其他对象拓展复用(测试2)

// 测试1

Event.on('test', function (result) {

    console.log(result);

});

Event.on('test', function () {

    console.log('test');

});

Event.emit('test', 'hello world'); // 输出 'hello world' 和 'test'

// 测试2

var person1 = {};

var person2 = {};

Object.assign(person1, Event);

Object.assign(person2, Event);

person1.on('call1', function () {

    console.log('person1');

});

person2.on('call2', function () {

    console.log('person2');

});

person1.emit('call1'); // 输出 'person1'

person1.emit('call2'); // 没有输出

person2.emit('call1'); // 没有输出

person2.emit('call2'); // 输出 'person2'<br>var Event = {

    // 通过on接口监听事件eventName

    // 如果事件eventName被触发,则执行callback回调函数

    on: function (eventName, callback) {

        //你的代码

    },

    // 触发事件 eventName

    emit: function (eventName) {

        //你的代码

    }

};


差点没把我看晕…


好吧,一步一步来看看怎么回事。


①了解一下观察者模式


观察者模式:


这是一种创建松散耦合代码的技术。它定义对象间 一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。由主体和观察者组成,主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。主体并不知道观察者的任何事情,观察者知道主体并能注册事件的回调函数。


例子:


假如我们正在开发一个商城网站,网站里有header头部、nav导航、消息列表、购物车等模块。这几个模块的渲染有一个共同的前提条件,就是必须先用ajax异步请求获取用户的登录信息。这是很正常的,比如用户的名字和头像要显示在header模块里,而这两个字段都来自用户登录后返回的信息。这个时候,我们就可以把这几个模块的渲染事件都放到一个数组里面,然后待登录成功之后再遍历这个数组并且调用每一个方法。

基本模式:


function EventTarget(){    

    this.handlers = {};

}

EventTarget.prototype = {    

    constructor: EventTarget,

    addHandler: function(type, handler){

         if (typeof this.handlers[type] == "undefined"){

              this.handlers[type] = [];

         }

         this.handlers[type].push(handler);

     },

    fire: function(event){

         if (!event.target){

             event.target = this;

         }

         if (this.handlers[event.type] instanceof Array){

             var handlers = this.handlers[event.type];

             for (var i=0, len=handlers.length; i < len; i++){

                 handlers[i](event);

            }

         }

     },

     removeHandler: function(type, handler){

        if (this.handlers[type] instanceof Array){

            var handlers = this.handlers[type];

            for (var i=0, len=handlers.length; i < len; i++){

                if (handlers[i] === handler){

                    break;

                 }

             }

             handlers.splice(i, 1);

          }

      }

};


大概意思就是,创建一个事件管理器。handles是一个存储事件处理函数的对象。


addHandle:是添加事件的方法,该方法接收两个参数,一个是要添加的事件的类型,一个是这个事件的回调函数名。调用的时候会首先遍历handles这个对象,看看这个类型的方法是否已经存在,如果已经存在则添加到该数组,如果不存在则先创建一个数组然后添加。


fire方法:是执行handles这个对象里面的某个类型的每一个方法。


removeHandle:是相应的删除函数的方法。


好啦,回到题目,分析一下。


②题目中的测试一:


// 测试1

Event.on('test', function (result) {

    console.log(result);

});

Event.on('test', function () {

    console.log('test');

});

Event.emit('test', 'hello world'); // 输出 'hello world' 和 'test'


意思就是,定义一个叫’test’类型的事件集,并且注册了两个test事件。然后调用test事件集里面的全部方法。在这里on方法等价于addHandle方法,emit方法等价于fire方法。其中第一个参数就是事件类型,第二个参数就是要传进函数的参数。


是不是这个回事呢?很好,那么我们要写的代码就是:


var Event = {

    // 通过on接口监听事件eventName

    // 如果事件eventName被触发,则执行callback回调函数

    on: function (eventName, callback) {

        //我的代码

        if(!this.handles){

             this.handles={};    

        }      

       if(!this.handles[eventName]){

            this.handles[eventName]=[];

       }

       this.handles[eventName].push(callback);

    },

    // 触发事件 eventName

    emit: function (eventName) {

        //你的代码

       if(this.handles[arguments[0]]){

           for(var i=0;i<this.handles[arguments[0]].length;i++){

               this.handles[arguments[0]][i](arguments[1]);

           }

       }

    }

};


这样测试,完美地通过了测试一。


③测试二:


var person1 = {};

var person2 = {};

Object.assign(person1, Event);

Object.assign(person2, Event);

person1.on('call1', function () {

    console.log('person1');

});

person2.on('call2', function () {

    console.log('person2');

});

person1.emit('call1'); // 输出 'person1'

person1.emit('call2'); // 没有输出

person2.emit('call1'); // 没有输出

person2.emit('call2'); // 输出 'person2'


大概意思就是为两个不同person注册自定义事件,并且两个person之间是互相独立的。


直接测试,发现输出了



这个好像是题目要求有点出入呢,或者这才是题目的坑吧!


解释一下,Object.assign(person1, Event);


这个是ES6的新对象方法,用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。


意思是将Event里面的可枚举的对象和方法放到person1里面。



也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。由于进行测试一的时候调用了on方法,所以event里面已经有了handles这个可枚举的属性。然后再分别合并到两个person里面的话,两个person对象里面的handles都只是一个引用。所以就互相影响了。


如果assign方法要实现深克隆则要这样:



问题是,题目已经固定了方式,我们不能修改这个方法。


所以,我们必须将handles这个属性定义为不可枚举的,然后在person调用on方法的时候再分别产生handles这个对象。


也就是说正确的做法应该是:


var Event = {

    // 通过on接口监听事件eventName

    // 如果事件eventName被触发,则执行callback回调函数

    on: function (eventName, callback) {

        //你的代码

        if(!this.handles){

            //this.handles={};

            Object.defineProperty(this, "handles", {

                value: {},

                enumerable: false,

                configurable: true,

                writable: true

            })

        }

      

       if(!this.handles[eventName]){

            this.handles[eventName]=[];

       }

       this.handles[eventName].push(callback);

    },

    // 触发事件 eventName

    emit: function (eventName) {

        //你的代码

       if(this.handles[arguments[0]]){

           for(var i=0;i<this.handles[arguments[0]].length;i++){

               this.handles[arguments[0]][i](arguments[1]);

           }

       }

    }

};


通过这道题,感觉考得真的很巧妙而且很考基础。好啦。我还是好好复习去了。



专栏作者简介 ( 点击 → 加入专栏作者 


winty:前端工程师,前端爱好者。博客:http://www.cnblogs.com/LuckyWinty/

打赏支持作者写出更多好文章,谢谢!




【今日微信公号推荐↓】

更多推荐请看值得关注的技术和设计公众号


其中推荐了包括技术设计极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!



 
前端大全 更多文章 使用纯前端JavaScript 实现Excel IO 3个Web入门小游戏,制作只需基础三剑客 JS 统治世界,面包机也不例外:重温 10 篇前端技术热文 怎么找工作,怎么找到工作,怎么找到满意工作? HTTP,HTTP2.0,SPDY,HTTPS你应该知道的一些事
猜您喜欢 如何在大学里成为IT技术大牛 喜欢周星驰 不论什么时候 不论别人怎么说(漫画) 可是姑娘,你为什么要编程呢? 程序员你为什么迷茫? 『互联网专车出行数据安全之301亲身体验』