65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Node.js 中编写自己的事件触发器:分步指南

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2019年2月15日

CPOL

6分钟阅读

viewsIcon

13150

通过从零开始编写内置 Node.js 事件触发器的小型实现,了解 Node 内部机制。

引言

这是一篇关于如何在 Node.js 中编写自己的事件触发器的分步指南。

如果您是 Node.js 的新手,Medium 和其他地方有许多教程。例如,您可以查看我的文章《深入了解核心 Node.JS》

言归正传,让我们开始讨论“事件触发器”。事件触发器在 Node.js 生态系统中扮演着非常重要的角色。

EventEmitter 是一个模块,它促进了 Node 中对象之间的通信/交互。EventEmitter 是 Node 异步事件驱动架构的核心。许多 Node 的内置模块都继承自 EventEmitter,包括像 Express.js 这样著名的框架。

这个概念非常简单:emitter 对象发出命名事件,这些事件会导致之前注册的监听器被调用。因此,一个 emitter 对象基本上有两个主要功能

  • 发出命名事件
  • 注册和注销监听器函数

它有点像发布/订阅或观察者设计模式(尽管不完全是)。

我们将在本教程中构建什么

  • EventEmitter
  • on / addEventListener 方法
  • off / removeEventListener 方法
  • once 方法
  • emit 方法
  • rawListeners 方法
  • listenerCount 方法

上述基本功能足以使用事件模型实现一个完整的系统。

在开始编码之前,让我们先看看我们将如何使用 EventEmitter 类。请注意,我们的代码将模仿 Node.js“events”模块的精确 API。

事实上,如果您用 Node.js 的内置“events”模块替换我们的 EventEmitter,您将获得相同的结果。

示例 1 — 创建一个事件触发器实例并注册两个回调函数

const myEmitter = new EventEmitter();

function c1() {
   console.log('an event occurred!');
}

function c2() {
   console.log('yet another event occurred!');
}

myEmitter.on('eventOne', c1); // Register for eventOne
myEmitter.on('eventOne', c2); // Register for eventOne

当事件“eventOne”触发时,上述两个回调函数都应该被调用。

myEmitter.emit('eventOne');

控制台中的输出将如下所示

an event occurred!
yet another event occurred!

示例 2 — 使用 Once 注册事件仅触发一次

myEmitter.once('eventOnce', () => console.log('eventOnce once fired'));  

触发事件“eventOnce

myEmitter.emit('eventOne');

控制台中应出现以下输出

eventOnce once fired

再次触发用 once 注册的事件将没有影响。

myEmitter.emit('eventOne');

由于事件只触发了一次,所以上述语句将没有影响。

示例 3 — 注册带回调参数的事件

myEmitter.on('status', (code, msg)=> console.log(`Got ${code} and ${msg}`));

触发带参数的事件

myEmitter.emit('status', 200, 'ok');

控制台中的输出将如下所示

Got 200 and ok

注意:您可以多次触发事件(除了使用 once 方法注册的事件)。

示例 4 — 注销事件

myEmitter.off('eventOne', c1);

现在,如果您按如下方式触发事件,什么都不会发生,这将是一个空操作

myEmitter.emit('eventOne');  // noop

示例 5 — 获取监听器数量

console.log(myEmitter.listenerCount('eventOne'));

注意:如果事件已使用 offremoveListener 方法注销,则计数将为 0

示例 6 — 获取原始监听器

console.log(myEmitter.rawListeners('eventOne'));

示例 7 — 异步示例演示

// Example 2->Adapted and thanks to Sameer Buna
class WithTime extends EventEmitter {
  execute(asyncFunc, ...args) {
    this.emit('begin');
    console.time('execute');
    this.on('data', (data)=> console.log('got data ', data));
    asyncFunc(...args, (err, data) => {
      if (err) {
        return this.emit('error', err);
      }
      this.emit('data', data);
      console.timeEnd('execute');
      this.emit('end');
    });
  }
}

使用 withTime 事件触发器

const withTime = new WithTime();

withTime.on('begin', () => console.log('About to execute'));
withTime.on('end', () => console.log('Done with execute'));

const readFile = (url, cb) => {
  fetch(url)
    .then((resp) => resp.json()) // Transform the data into json
    .then(function(data) {
      cb(null, data);
    });
}

withTime.execute(readFile, 'https://jsonplaceholder.typicode.com/posts/1');

检查控制台中的输出。帖子列表将与其他日志一起显示。

我们事件触发器的观察者模式

视觉图 1(我们 EventEmitter 中的方法)

既然我们现在了解了使用 API,让我们开始编写模块。

EventEmitter 类的完整样板代码

我们将在接下来的几节中逐步填写详细信息。

class EventEmitter {
  listeners = {};  // key-value pair

  addListener(eventName, fn) {}
  on(eventName, fn) {}

  removeListener(eventName, fn) {}
  off(eventName, fn) {}

  once(eventName, fn) {}

  emit(eventName, ...args) { }

  listenerCount(eventName) {}

  rawListeners(eventName) {}
}

我们首先创建 EventEmitter 类的模板以及一个用于存储监听器的哈希表。监听器将以键值对的形式存储。值可以是一个数组(因为对于同一个事件,我们允许注册多个监听器)。

1. addListener() 方法

现在让我们实现 addListener 方法。它接受一个事件名称和一个要执行的回调函数。

  addListener(event, fn) {
    this.listeners[event] = this.listeners[event] || [];
    this.listeners[event].push(fn);
    return this;
  }

一点解释

addListener 事件检查事件是否已注册。如果已注册,则返回数组,否则返回空数组。

this.listeners[event] // will return array of events or undefined (first time registration)

例如……

让我们通过一个使用示例来理解这一点。让我们创建一个新的 eventEmitter 并注册一个“test-event”。这是“test-event”第一次被注册。

const eventEmitter = new EventEmitter();
eventEmitter.addListener('test-event', 
 ()=> { console.log ("test one") } 
);

addListener() 方法内部

this.listeners[event] =>  this.listeners['test-event'] 
                  => undefined || []
                  => []

结果将是。

this.listeners['test-event'] = [];  // empty array

然后“fn”将被推入此数组,如下所示

this.listeners['test-event'].push(fn);

我希望这使得“addListener”方法非常清晰易懂。

注意:可以针对同一个事件注册多个回调函数。

2. on 方法

这只是“addListener”方法的别名。为了方便起见,我们将更多地使用“on”方法而不是“addListener”方法。

on(event, fn) {
  return this.addListener(event, fn);
}

3. removeListener(event, fn) 方法

removeListener 方法将 eventName 和回调函数作为参数。它从事件数组中移除所述监听器。

注意:如果事件有多个监听器,则其他监听器将不受影响。

首先,让我们看看 removeListener 的完整代码。

removeListener (event, fn) {
    let lis = this.listeners[event];
    if (!lis) return this;
    for(let i = lis.length; i > 0; i--) {
      if (lis[i] === fn) {
        lis.splice(i,1);
        break;
      }
    }
    return this;
}

这是 removeListener 方法的分步解释

  • 通过“event”获取监听器数组
  • 如果未找到,则返回“this”以进行链式调用。
  • 如果找到,则遍历所有监听器。如果当前监听器与“fn”参数匹配,则使用数组的 splice 方法将其删除。跳出循环。
  • 返回“this”以继续链式调用。

4. off(event, fn) 方法

这只是“removeListener”方法的别名。为了方便起见,我们将更多地使用“on”方法而不是“addListener”方法。

  off(event, fn) {
    return this.removeListener(event, fn);
  }

5. once(eventName, fn) 方法

为名为 eventName 的事件添加一个一次性 listener 函数。下次触发 eventName 时,此监听器将被移除并调用。

用于设置/初始化类事件。

我们来看看代码

once(eventName, fn) {
    this.listeners[event] = this.listeners[eventName] || [];
    const onceWrapper = () => {
      fn();
      this.off(eventName, onceWrapper);
    }
    this.listeners[eventName].push(onceWrapper);
    return this;
}

以下是 once 方法的分步解释

  • 获取事件数组对象。如果是第一次,则为空数组。
  • 创建一个名为 onceWrapper 的包装函数,该函数将在事件触发时调用 fn 并同时移除监听器。
  • 将包装函数添加到数组中。
  • 返回“this”以进行链式调用。

6. emit (eventName, ..args) 方法

同步调用为名为 eventName 的事件注册的每个监听器,按它们注册的顺序,并将提供的参数传递给每个监听器。

如果事件有监听器,则返回 true,否则返回 false

emit(eventName, ...args) {
    let fns = this.listeners[eventName];
    if (!fns) return false;
    fns.forEach((f) => {
      f(...args);
    });
    return true;
}

以下是 emit 方法的分步解释

  • 获取所述 eventName 参数的函数
  • 如果没有监听器,则返回 false
  • 对于所有函数监听器,使用参数调用该函数
  • 完成后返回 true

7. listenerCount (eventName) 方法

返回监听名为 eventName 的事件的监听器数量。

这是源代码

listenerCount(eventName) {
    let fns = this.listeners[eventName] || [];
    return fns.length;
}

以下是 listenerCount 方法的分步解释

  • 获取正在考虑的函数/监听器,如果没有则返回空数组。
  • 返回长度。

8. rawListeners(eventName) 方法

返回名为 eventName 的事件的监听器数组的副本,包括任何包装器(例如由 .once() 创建的包装器)。如果事件已触发一次,则此实现中的 once 包装器将不再可用。

rawListeners(event) {
    return this.listeners[event];
}

供参考的完整源代码

class EventEmitter {
  listeners = {}
  
  addListener(eventName, fn) {
    this.listeners[eventName] = this.listeners[eventName] || [];
    this.listeners[eventName].push(fn);
    return this;
  }

  on(eventName, fn) {
    return this.addListener(eventName, fn);
  }

  once(eventName, fn) {
    this.listeners[eventName] = this.listeners[eventName] || [];
    const onceWrapper = () => {
      fn();
      this.off(eventName, onceWrapper);
    }
    this.listeners[eventName].push(onceWrapper);
    return this;
  }

  off(eventName, fn) {
    return this.removeListener(eventName, fn);
  }

  removeListener (eventName, fn) {
    let lis = this.listeners[eventName];
    if (!lis) return this;
    for(let i = lis.length; i > 0; i--) {
      if (lis[i] === fn) {
        lis.splice(i,1);
        break;
      }
    }
    return this;
  }

  emit(eventName, ...args) {
    let fns = this.listeners[eventName];
    if (!fns) return false;
    fns.forEach((f) => {
      f(...args);
    });
    return true;
  }

  listenerCount(eventName) {
    let fns = this.listeners[eventName] || [];
    return fns.length;
  }

  rawListeners(eventName) {
    return this.listeners[eventName];
  }
}

完整的代码可在以下链接获取

作为一项练习,您可以随意实现文档中其他事件的 API:https://node.org.cn/api/events.html

如果您喜欢这篇文章并希望看到更多类似文章,请点赞。

最初发布于 freecodecamp.org

注意:代码已针对可读性进行了优化,而不是性能。也许作为一项练习,您可以优化代码并在评论部分分享。我没有完全测试边缘情况,并且一些验证可能不准确,因为这是一篇快速撰写的文章。

本文是即将推出的视频课程“Node.JS 大师班——从零开始构建自己的 ExpressJS 类 MVC 框架”的一部分。

课程名称尚未最终确定。

我的 Twitter 视觉笔记 https://twitter.com/rajeshpillai

© . All rights reserved.