首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >事件发射器npm模块

事件发射器npm模块
EN

Code Review用户
提问于 2018-08-09 20:36:50
回答 1查看 153关注 0票数 5

我被要求完成以下任务:

事件发射器是事件驱动体系结构中充当核心构建块的对象。它们简化了处理异步事件的过程,并启用了干净和解耦的代码。用文档和测试在JavaScript中创建一个事件发射器模块(您喜欢的版本一样现代化)。你的实施应考虑到:

  • 使用任意数量的参数发出命名事件。
  • 为传递适当的发射参数的命名事件注册处理程序函数。
  • 注册最多一次调用的“一次性”处理程序。
  • 删除特定的先前注册的事件处理程序和/或所有以前注册的事件处理程序。

这个模块应该适合发布到npm,虽然没有必要这样做。不需要子类或其他方式需要现有的事件发射器模块,并且除了测试或开发依赖项之外,不包含任何依赖项。

下面是我创建的模块,以及我创建模块时开发的单元测试。它类似于NodeJS事件发射器API,但显然没有那么复杂。

模块和测试代码的外观如何?你会建议采取什么不同的做法?

发射极模块:

代码语言:javascript
运行
复制
"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);
}

单元测试

代码语言:javascript
运行
复制
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;    
  });
});
EN

回答 1

Code Review用户

发布于 2018-08-11 19:38:47

{}

的注意事项

在JavaScript中,{}经常用作映射,用户提供的值是对象属性名。这可能会导致灾难。请参阅这段代码,这会导致错误:

代码语言:javascript
运行
复制
var ev = new Emitter();

ev.once("toString", function () {
    console.log("Oops!");
});

ev.emit("toString");

我们将得到的错误来自使用addHandlerin运算符函数。

如果指定的属性位于指定的对象or中,则in运算符返回true,其原型链

当使用事件"toString“调用toString时,它会检查"toString" in this.eventHandlers是否。它在Object.prototype中找到了一个,因为{}继承了它。从现在起我们就有麻烦了。

如果我们将in替换为hasOwnProperty调用,那么当有人认为hasOwnProperty是一个事件的好名称时,我们仍然会遇到麻烦:

代码语言:javascript
运行
复制
var ev = new Emitter();

ev.once("hasOwnProperty", function () {
    console.log("Still in trouble!");
});

ev.emit("hasOwnProperty");

之后,所有this.eventHandlers.hasOwnProperty表达式都将计算为处理程序数组:

代码语言:javascript
运行
复制
var a = { hasOwnProperty: [] };
a.hasOwnProperty();

为了安全地使用用户提供的属性名对对象调用hasOwnProperty,我们应该直接使用来自Object.prototype的函数:

代码语言:javascript
运行
复制
// JavaScript best practices, so sweet
Object.prototype.hasOwnProperty.call(this.eventHandlers, event);

实际上,我们想要调用的任何标准方法都是相同的。

相反,我建议您使用Object.create(null)。它将在没有原型的情况下创建一个空对象,因此使用托架查找表示法可以安全地避免所有hasOwnProperty混乱。

看到它的行动:

代码语言:javascript
运行
复制
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方法有一种特殊的失败情况:

代码语言:javascript
运行
复制
var ev = new Emitter();

ev.once("foo", function () {
    console.log("Hey, I was called!");
    ev.emit("foo");
});

ev.emit("foo");

我建议您在调用一次性处理程序之前删除它。

off

off方法移除已注册的事件处理程序。但是,如果我们直接从事件处理程序调用它呢:

代码语言:javascript
运行
复制
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]中的下一个事件处理程序:

代码语言:javascript
运行
复制
var foo = { bar: [1, 2] };

for (var i = 0; i < foo.bar.length; i++) {
  console.log(foo.bar[i]);
  delete foo.bar;
}

回调比较

off方法允许以下内容:

代码语言:javascript
运行
复制
ev.on("foo", function() { /* handler code */ });
ev.off("foo", function() { /* handler code */ });

删除事件处理程序的正确方法包括存储处理程序函数,然后使用off方法:

代码语言:javascript
运行
复制
var handler = function() { /* handler code */ };
ev.on("foo", handler);
ev.off("foo", handler);

您正在通过它们的字符串表示来比较函数:

代码语言:javascript
运行
复制
callback.toString() == this.eventHandlers[event][index].callback.toString()

实际上,您可以直接比较功能:

代码语言:javascript
运行
复制
callback === this.eventHandlers[event][index].callback

示例:

代码语言:javascript
运行
复制
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]进行切片并对其进行迭代。

代码语言:javascript
运行
复制
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++)
票数 3
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/201326

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档