DaraW

Code is Poetry

JS实现自定义事件

要求

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 测试 1
Event.on("test", function(result) {
console.log(result);
})
Event.on("test", function(result) {
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”

思路

这是个经典的自定义事件,实现”Pub/Sub”,Node.js中有个比较完善的实现EventEmitter

原理则是构造出一个集成队列的对象,每一个事件对应对象的一个队列,在自定义事件后将回调函数入队,触发事件后回调函数依次出队并执行。

个人偏爱IIFE+构造器模式+原型模式,于是有了下面的一个简易实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(function (exports) {

function EventEmitter() {}

EventEmitter.prototype.on = function (type, handler) {
this._cbQueue = this._cbQueue || {};
this._cbQueue[type] = this._cbQueue[type]|| [];
this._cbQueue[type].push(handler);
return this;
};

EventEmitter.prototype.emit = function (type, data) {
if (this._cbQueue[type]) {
this._cbQueue[type].forEach(function (cb) {
cb(data);
});
}
};

exports.EventEmitter = EventEmitter;

}(global));

var Event = new EventEmitter();

// Test 1
Event.on("test", function(result) {
console.log(result);
})
Event.on("test", function(result) {
console.log("test");
})
Event.emit("test", "hello world"); // 输出“hello world”和”test”

这样便满足了测试1的要求,然而这样对于测试2是不行的,因为Object.assign()方法不能复制不可遍历的属性和继承属性,也就意味着Event对象上的onemit不能被复制过去。那么复制Event.prototype对象呢?这样确实是可行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Test2
var person1 = {};
var person2 = {};

Object.assign(person1, EventEmitter.prototype);
Object.assign(person2, EventEmitter.prototype);

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”

虽然差不多实现了题目中的要求,但是和题目中的要求依然有些偏差,于是我又想到了,如果放弃原型模式+构造器模式,单纯的把Event作为一个对象而不是一个函数呢?
实现很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
(function (exports) {

var Event = {};

Event.on = function (type, handler) {
this._cbQueue = this._cbQueue || {};
this._cbQueue[type] = this._cbQueue[type]|| [];
this._cbQueue[type].push(handler);
return this;
};

Event.emit = function (type, data) {
if (this._cbQueue[type]) {
this._cbQueue[type].forEach(function (cb) {
cb(data);
});
}
};

exports.Event = Event;

}(global));


// Test 1
Event.on("test", function(result) {
console.log(result);
})
Event.on("test", function(result) {
console.log("test");
})
Event.emit("test", "hello world"); // 输出“hello world”和”test”


//Test2
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”
person2.emit("call1"); // 输出”person1”
person2.emit("call2"); // 输出”person2”

测试1的结果如预期一样,然而测试2却出了问题,debug看了一下Event对象,Event.on执行后上面竟然有个可以遍历的属性_cbQueue,而且_cbQueue是一个对象而不是一个字面量,所以在Object.assign()拷贝的过程中,将Event._cbQueue对象引用赋值给了person1._cbQueueperson2._cbQueue,也就是说这三者指向了内存中的同一个对象,只要修改一个,其他几个都会跟着修改;当不执行Event.on时,Event上就不会有属性_cbQueue,那么接下来person1person2执行on方法后,this指向了他们本身,会创造他们自己的互不影响的_cbQueue属性。
但不执行Event.on这不是解决的完美办法,不过问题已经定位了,解决也很简单,修改Event对象的属性_cbQueue为不可遍历,让拷贝过程不拷贝属性_cbQueue即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
(function (exports) {

var Event = {};

Object.defineProperty(Event, "_cbQueue", {
value : {},
writable : true,
enumerable : false,
configurable : true
});

Event.on = function (type, handler) {
this._cbQueue = this._cbQueue || {};
this._cbQueue[type] = this._cbQueue[type]|| [];
this._cbQueue[type].push(handler);
return this;
};

Event.emit = function (type, data) {
if (this._cbQueue[type]) {
this._cbQueue[type].forEach(function (cb) {
cb(data);
});
}
};

exports.Event = Event;

}(global));


// Test 1
Event.on("test", function(result) {
console.log(result);
})
Event.on("test", function(result) {
console.log("test");
})
Event.emit("test", "hello world"); // 输出“hello world”和”test”

//Test2
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”

这样便算完美实现了题目的要求。

总结

其实原理很简单,没想到在周边的实现上浪费了一些debug的时间,有些惭愧。

Proudly powered by Hexo and Theme by Hacker
© 2022 DaraW