超详细的JavaScript 的观察者模式

在网上看到一篇观察者模式的面试题

请实现下面的自定义事件 Event 对象的接口,功能见注释(测试1)
该 Event 对象的接口需要能被其他对象拓展复用(测试2)

var Event = {
    // 通过on接口监听事件eventName
    // 如果事件eventName被触发,则执行callback回调函数
    on: function (eventName, callback) {
        //你的代码
    },
    // 触发事件 eventName
    emit: function (eventName) {
        //你的代码
    }
};

测试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'); // 没有输出

测试1问题解决

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

完成题目中的代码:

var Event = {
    on: function (eventName, handler) {
        if (!this.handler){
            this.handler = {}
        }
        if (!this.handler[eventName]){
            this.handler[eventName] = []
        }
        this.handler[eventName].push(handler)
    },
    emit: function (eventName){
        if (!this.handler[eventName]){
            return false
        } else {
            for(var i = 0; i<this.handler[eventName].length;i++){
                this.handler[eventName][i](arguments(1))
            }
        }
    }
}

分析下上面代码的功能:
首先Event就是创建观察者模式的那个对象,对象的on方法用来订阅事件。假设我们用this.handler={}来存放所有的订阅事件,在测试1中执行了2次on方法,

Event.on('test', function (result) {
    console.log(result);
});
Event.on('test', function () {
    console.log('test');
});

执行之后this.handler={
test: [function(result){console.log(result)},function(){console.log(‘test’)}]
}
这时候执行Event的emit方法,就会得到相应的输出

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

测试2问题解决
在把题目中的代码补充完成后,继续把测试2中的代码放下面继续执行

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

我们发现后面两个emit方法都有输出,而测试2中要求后面两个emit方法不能有输出。
先分析下出现上面结果的原因:
Object.assign()是ES6的语法,用于对象的合并,把源对象的可枚举属性复制到目标对象。意思是将Event里面的可枚举的对象和方法放到person1和person2里面。
如果源对象某个属性的值是对象,复制到目标对象中的是该属性的引用(该复制只是浅复制)。由于进行测试1的时候调用了on方法,对Event对象的handler属性进行了操作,而后又把该属性的引用付给了person1和person2,也都分别往里添加了新的订阅事件。所以最后handler的值为:

扫描二维码关注公众号,回复: 876307 查看本文章
this.handler={
    test: [function(result){console.log(result)},function(){console.log('test')}],
    call1:[function(){console.log('call1')}],
    call2:[function(){console.log('call2')}]
}

所以最后调用emit的时候,不管是哪个对象的哪个方法都可以调用到。

这里要实现测试2中的结果,关键问题在于使用Object.assign语法属性对象浅复制的问题,由于Object.assign语法不可更改只能是浅复制,所以我们可以把对象中的handler属性变成不可枚举的,这样就能实现属性的深复制了。

枚举属性和不可枚举属性
最后修改好的代码:

var Event = {
    on: function (eventName, handler) {
        if (!this.handler){
            Object.defineProperty(this, 'handler', {
                configurable: true,
                enumerable: false,
                value: {},
                writable: true
            })
        }
        if (!this.handler[eventName]){
            this.handler[eventName] = []
        }
        this.handler[eventName].push(handler)
        console.log(this.handler)
    },
    emit: function (eventName){
        let fnArr = this.handler[eventName]
        console.log(fnArr)
        if (!fnArr){
            return false
        } else {
            for(let i = 0; i < fnArr.length; i++){
                fnArr[i](arguments[1])
            }
        }
    }
}

The End


下面是一些参考的网站

面试题来源http://web.jobbole.com/87639/
浅析JavaScript设计模式——发布-订阅/观察者模式https://blog.csdn.net/q1056843325/article/details/53353850

猜你喜欢

转载自blog.csdn.net/one_girl/article/details/80325210