我被要求完成以下任务:
事件发射器是事件驱动体系结构中充当核心构建块的对象。它们简化了处理异步事件的过程,并启用了干净和解耦的代码。用文档和测试在JavaScript中创建一个事件发射器模块(您喜欢的版本一样现代化)。你的实施应考虑到:
这个模块应该适合发布到npm,虽然没有必要这样做。不需要子类或其他方式需要现有的事件发射器模块,并且除了测试或开发依赖项之外,不包含任何依赖项。
下面是我创建的模块,以及我创建模块时开发的单元测试。它类似于NodeJS事件发射器API,但显然没有那么复杂。
模块和测试代码的外观如何?你会建议采取什么不同的做法?
"use strict";
function Emitter() {
//constructor
this.eventHandlers = {};
}
/**
* Emit an event
* @param event string
* @param arg1...argN - any arguments to be sent to callback functions
*/
Emitter.prototype.emit = function(event) {
const args = Array.from(arguments).slice(1);
if (this.eventHandlers.hasOwnProperty(event)) {
let indexesToRemove = [];
for (const index in this.eventHandlers[event]) {
const handler = this.eventHandlers[event][index];
handler.callback.apply(null, args);
if (handler.hasOwnProperty('once')) {
indexesToRemove.push(index);
}
}
if (indexesToRemove.length) {
for(const index in indexesToRemove) {
this.eventHandlers[event].splice(index, 1);
}
}
}
};
/**
* Register a callback for an event
* @param event
* @param callback
*/
Emitter.prototype.on = function(event, callback) {
addHandler.call(this, event, {callback: callback});
};
/**
* Register a callback for an event to be called only once
* @param event
* @param callback
*/
Emitter.prototype.once = function(event, callback) {
addHandler.call(this, event, {callback: callback, once: true});
};
/**
* Un-register a single or all callbacks for a given event
* @param event
* @param callback optional
*/
Emitter.prototype.off = function(event, callback) {
if (this.eventHandlers.hasOwnProperty(event)) {
if (callback !== undefined) {
for (const index in this.eventHandlers[event]) {
if (callback.toString() == this.eventHandlers[event][index].callback.toString()) {
this.eventHandlers[event].splice(index, 1);
}
}
}
else {
delete this.eventHandlers[event];
}
}
}
module.exports = Emitter;
/**
* Helper function to add an event handler
* @param event
* @param handlerObject
*/
function addHandler(event, handlerObject) {
if (!(event in this.eventHandlers)) {
this.eventHandlers[event] = [];
}
this.eventHandlers[event].push(handlerObject);
}
const EventEmitter = require("../src/");
const chai = require("chai");
const sinon = require('sinon');
const sinonChai = require("sinon-chai");
chai.should();
chai.use(sinonChai);
describe("Event Emitter", function() {
let emitter;
before(function() {
emitter = new EventEmitter();
});
it("Allows registering handler functions for named events that are passed the appropriate arguments on emission.", function() {
const callback = sinon.fake();
emitter.on('scrape', callback);
emitter.emit('scrape');
callback.should.have.been.called;
const testValue1 = 'testValue1';
emitter.emit('scrape', testValue1);
callback.should.have.been.calledWith(testValue1);
const multiArgs = ['a', 'scraped', 'value'];
emitter.emit('scrape', ...multiArgs);
callback.should.have.been.calledWith(...multiArgs);
});
it("Allows Registering a \"one-time\" handler that will be called at most one time.", function() {
const callback = sinon.fake();
const callback2 = sinon.fake();
emitter.once('pull', callback);
emitter.on('pull', callback2);
emitter.emit('pull');
emitter.emit('pull');
callback.should.have.been.calledOnce;
callback2.should.have.been.calledTwice;
});
it("Allows Removing specific previously-registered event handlers and/or all previously-registered event handlers.", function() {
const callback = sinon.fake();
const callback2 = sinon.fake();
const callback3 = sinon.fake();
const callback4 = sinon.fake();
emitter.on('push', callback);
emitter.on('push', callback2);
emitter.off('push', callback);
emitter.emit('push');
callback.should.not.have.been.called;
callback2.should.have.been.called;
emitter.on('push', callback3);
emitter.off('push');
emitter.emit('push');
emitter.on('unshift', callback4);
emitter.emit('unshift');
callback3.should.not.have.been.called;
callback4.should.have.been.called;
});
});
发布于 2018-08-11 19:38:47
{}
的注意事项
在JavaScript中,{}
经常用作映射,用户提供的值是对象属性名。这可能会导致灾难。请参阅这段代码,这会导致错误:
var ev = new Emitter();
ev.once("toString", function () {
console.log("Oops!");
});
ev.emit("toString");
我们将得到的错误来自使用addHandler
的in
运算符函数。
如果指定的属性位于指定的对象or中,则in运算符返回true,其原型链。
当使用事件"toString“调用toString时,它会检查"toString" in this.eventHandlers
是否。它在Object.prototype
中找到了一个,因为{}
继承了它。从现在起我们就有麻烦了。
如果我们将in
替换为hasOwnProperty
调用,那么当有人认为hasOwnProperty是一个事件的好名称时,我们仍然会遇到麻烦:
var ev = new Emitter();
ev.once("hasOwnProperty", function () {
console.log("Still in trouble!");
});
ev.emit("hasOwnProperty");
之后,所有this.eventHandlers.hasOwnProperty
表达式都将计算为处理程序数组:
var a = { hasOwnProperty: [] };
a.hasOwnProperty();
为了安全地使用用户提供的属性名对对象调用hasOwnProperty
,我们应该直接使用来自Object.prototype
的函数:
// JavaScript best practices, so sweet
Object.prototype.hasOwnProperty.call(this.eventHandlers, event);
实际上,我们想要调用的任何标准方法都是相同的。
相反,我建议您使用Object.create(null)
。它将在没有原型的情况下创建一个空对象,因此使用托架查找表示法可以安全地避免所有hasOwnProperty
混乱。
看到它的行动:
var a = {};
console.log("a = {}");
console.log("constructor" in a, "toString" in a, "hasOwnProperty" in a);
var a = Object.create(null);
console.log("a = Object.create(null)");
console.log("constructor" in a, "toString" in a, "hasOwnProperty" in a);
在StackOverflow答案上可以看到更多关于这一点的信息。
当同一个事件直接从事件处理程序发出时,once
方法有一种特殊的失败情况:
var ev = new Emitter();
ev.once("foo", function () {
console.log("Hey, I was called!");
ev.emit("foo");
});
ev.emit("foo");
我建议您在调用一次性处理程序之前删除它。
off
方法移除已注册的事件处理程序。但是,如果我们直接从事件处理程序调用它呢:
var ev = new Emitter();
ev.on("foo", function () {
console.log("First");
ev.off("foo");
});
ev.on("foo", function () {
console.log("Second");
});
ev.emit("foo");
这里有两个处理程序,用于同一个事件"foo“。当调用第一个处理程序时,它用delete this.eventHandlers[event]
删除所有"foo“处理程序。但是,当它返回时,我们仍然处于for循环中,试图访问最近被删除的this.eventHandlers[event]
中的下一个事件处理程序:
var foo = { bar: [1, 2] };
for (var i = 0; i < foo.bar.length; i++) {
console.log(foo.bar[i]);
delete foo.bar;
}
off
方法允许以下内容:
ev.on("foo", function() { /* handler code */ });
ev.off("foo", function() { /* handler code */ });
删除事件处理程序的正确方法包括存储处理程序函数,然后使用off
方法:
var handler = function() { /* handler code */ };
ev.on("foo", handler);
ev.off("foo", handler);
您正在通过它们的字符串表示来比较函数:
callback.toString() == this.eventHandlers[event][index].callback.toString()
实际上,您可以直接比较功能:
callback === this.eventHandlers[event][index].callback
示例:
var a = function() {
console.log(Math.PI);
}
var b = function() {
console.log(Math.PI);
}
var c = a;
console.log(function(){} !== function(){}); // different functions
console.log(a !== b); // different functions
console.log(a === c); // same function
console.log(a.toString() === b.toString()); // different functions,
// but same body
引用Node.js事件文档的话:
请注意,一旦事件被发出,在发出事件时附加到它的所有侦听器都将按顺序被调用。这意味着,任何removeListener()或removeAllListeners()调用在发出之后并在最后一个侦听器完成执行之前都不会从执行中的emit()中删除它们。
因此,简而言之,任何事情都不应该改变由触发事件处理程序组成的挂起数组。
要实现这一点,您可以对this.eventHandlers[event]
进行切片并对其进行迭代。
let handlers = this.eventHandlers[event].slice(0);
for (var i = 0; i < handlers.length; i++) {
const handler = handlers[i];
// ...
}
这将处理上面描述的off
错误。
if (handler.hasOwnProperty('once'))
有什么意义,而应该考虑if (handler.once)
。index
是循环变量的长名称,那么普通的旧i
如何?for (var i in arr)
表单用于对象属性名称上的迭代,其中迭代顺序没有得到保证。若要迭代数组,请改用for (var i = 0; i < arr.length; i++)
。https://codereview.stackexchange.com/questions/201326
复制相似问题