返回介绍

观察者模式

发布于 2025-01-25 22:50:10 字数 8112 浏览 0 评论 0 收藏 0

Node.js 中的另一个重要和基本的模式是观察者模式。与 reactor 模式 ,回调模式和模块一样,观察者模式是 Node.js 基础之一,也是使用许多 Node.js 核心模块和用户定义模块的基础。

观察者模式是对 Node.js 的数据响应的理想解决方案,也是对回调的完美补充。我们给出以下定义:

发布者定义一个对象,它可以在其状态发生变化时通知一组观察者(或监听者)。

与回调模式的主要区别在于,主体实际上可以通知多个观察者,而传统的 CPS 风格的回调通常主体的结果只会传播给一个监听器。

EventEmitter 类

在传统的面向对象编程中,观察者模式需要接口,具体类和层次结构。在 Node.js 中,都变得简单得多。观察者模式已经内置在核心模块中,可以通过 EventEmitter 类来实现。 EventEmitter 类允许我们注册一个或多个函数作为监听器,当特定的事件类型被触发时,它的回调将被调用,以通知其监听器。以下图像直观地解释了这个概念:

EventEmitter 是一个类(原型),它是从事件核心模块导出的。以下代码显示了如何获得对它的引用:

const EventEmitter = require('events').EventEmitter;
const eeInstance = new EventEmitter();

EventEmitter 的基本方法如下:

  • on(event,listener) :此方法允许您为给定的事件类型( String 类型 )注册一个新的侦听器(一个函数)
  • once(event, listener) :此方法注册一个新的监听器,然后在事件首次发布之后被删除
  • emit(event, [arg1], [...]) :此方法会生成一个新事件,并提供其他参数以传递给侦听器
  • removeListener(event, listener) :此方法将删除指定事件类型的侦听器

所有上述方法将返回 EventEmitter 实例以允许链接。监听器函数 function([arg1], [...]) ,所以它只是接受事件发出时提供的参数。在侦听器中,这是指 EventEmitter 生成事件的实例。 我们可以看到,一个监听器和一个传统的 Node.js 回调有很大的区别;特别地,第一个参数不是 error ,它是在调用时传递给 emit() 的任何数据。

创建和使用 EventEmitter

我们来看看我们如何在实践中使用 EventEmitter 。最简单的方法是创建一个新的实例并立即使用它。以下代码显示了在文件列表中找到匹配特定正则的文件内容时,使用 EventEmitter 实现实时通知订阅者的功能:

const EventEmitter = require('events').EventEmitter;
const fs = require('fs');

function findPattern(files, regex) {
  const emitter = new EventEmitter();
  files.forEach(function(file) {
    fs.readFile(file, 'utf8', (err, content) => {
      if (err)
        return emitter.emit('error', err);
      emitter.emit('fileread', file);
      let match;
      if (match = content.match(regex))
        match.forEach(elem => emitter.emit('found', file, elem));
    });
  });
  return emitter;
}

由前面的函数 EventEmitter 处理将产生的三个事件:

  • fileread 事件:当文件被读取时触发
  • found 事件:当文件内容被正则匹配成功时触发
  • error 事件:当读取文件出现错误时触发

下面看 findPattern() 函数是如何被触发的:

findPattern(['fileA.txt', 'fileB.json'], /hello \w+/g)
  .on('fileread', file => console.log(file + ' was read'))
  .on('found', (file, match) => console.log('Matched "' + match + '" in file ' + file))
  .on('error', err => console.log('Error emitted: ' + err.message));

在前面的例子中,我们为 EventParttern() 函数创建的 EventEmitter 生成的每个事件类型注册了一个监听器。

错误传播

如果事件是异步发送的, EventEmitter 不能在异常情况发生时抛出异常,异常会在事件循环中丢失。相反,而是 emit 是发出一个称为错误的特殊事件, Error 对象 通过参数传递。这正是我们在之前定义的 findPattern() 函数中正在做的。

对于错误事件,始终是最佳做法注册侦听器,因为 Node.js 会以特殊的方式处理它,并且如果没有找到相关联的侦听器,将自动抛出异常并退出程序。

让任意对象可观察

有时,直接通过 EventEmitter 类创建一个新的可观察的对象是不够的,因为原生 EventEmitter 类并没有提供我们实际运用场景的拓展功能。我们可以通过扩展 EventEmitter 类使一个通用对象可观察。

为了演示这个模式,我们试着在对象中实现 findPattern() 函数的功能,如下代码所示:

const EventEmitter = require('events').EventEmitter;
const fs = require('fs');
class FindPattern extends EventEmitter {
  constructor(regex) {
    super();
    this.regex = regex;
    this.files = [];
  }
  addFile(file) {
    this.files.push(file);
    return this;
  }
  find() {
    this.files.forEach(file => {
      fs.readFile(file, 'utf8', (err, content) => {
        if (err) {
          return this.emit('error', err);
        }
        this.emit('fileread', file);
        let match = null;
        if (match = content.match(this.regex)) {
          match.forEach(elem => this.emit('found', file, elem));
        }
      });
    });
    return this;
  }
}

我们定义的 FindPattern 类中运用了核心模块 util 提供的 inherits() 函数来扩展 EventEmitter 。以这种方式,它成为一个符合我们实际运用场景的可观察类。以下是其用法的示例:

const findPatternObject = new FindPattern(/hello \w+/);
findPatternObject
  .addFile('fileA.txt')
  .addFile('fileB.json')
  .find()
  .on('found', (file, match) => console.log(`Matched "${match}"
       in file ${file}`))
  .on('error', err => console.log(`Error emitted ${err.message}`));

现在,通过继承 EventEmitter 的功能,我们现在可以看到 FindPattern 对象除了可观察外,还有一整套方法。 这在 Node.js 生态系统中是一个很常见的模式,例如,核心 HTTP 模块的 Server 对象定义了 listen()close()setTimeout() 等方法,并且在内部它也继承自 EventEmitter 函数,从而允许它在收到新的请求、建立新的连接或者服务器关闭响应请求相关的事件。

扩展 EventEmitter 的对象的其他示例是 Node.js 流。我们将在第五章中更详细地分析 Node.js 的流。

同步和异步事件

与回调模式类似,事件也支持同步或异步发送。至关重要的是,我们决不应当在同一个 EventEmitter 中混合使用两种方法,但是在发布相同的事件类型时考虑同步或者异步显得至关重要,以避免产生因同步与异步顺序不一致导致的 zalgo

发布同步和异步事件的主要区别在于观察者注册的方式。当事件异步发布时,即使在 EventEmitter 初始化之后,程序也会注册新的观察者,因为必须保证此事件在事件循环下一周期之前不被触发。正如上边的 findPattern() 函数中的情况。它代表了大多数 Node.js 异步模块中使用的常用方法。

相反,同步发布事件要求在 EventEmitter 函数开始发出任何事件之前就得注册好观察者。看下面的例子:

const EventEmitter = require('events').EventEmitter;
class SyncEmit extends EventEmitter {
  constructor() {
    super();
    this.emit('ready');
  }
}
const syncEmit = new SyncEmit();
syncEmit.on('ready', () => console.log('Object is ready to be  used'));

如果 ready 事件是异步发布的,那么上述代码将会正常运行,然而,由于事件是同步发布的,并且监听器在发送事件之后才被注册,所以结果不调用监听器,该代码将无法打印到控制台。

由于不同的应用场景,有时以同步方式使用 EventEmitter 函数是有意义的。因此,要清楚地突出我们的 EventEmitter 的同步和异步性,以避免产生不必要的错误和异常。

事件机制与回调机制的比较

在定义异步 API 时,常见的难点是检查是否使用 EventEmitter 的事件机制或仅接受回调函数。一般区分规则是这样的:当一个结果必须以异步方式返回时,应该使用回调函数,当需要结果不确定其方式时,应该使用事件机制来响应。

但是,由于这两者实在太相近,并且可能两种方式都能实现相同的应用场景,所以产生了许多混乱。以下列代码为例:

function helloEvents() {
  const eventEmitter = new EventEmitter();
  setTimeout(() => eventEmitter.emit('hello', 'hello world'), 100);
  return eventEmitter;
}

function helloCallback(callback) {
  setTimeout(() => callback('hello world'), 100);
}

helloEvents()helloCallback() 在其功能上可以被认为是等价的,第一个使用事件机制实现,第二个则使用回调来通知调用者,而将事件作为参数传递。但是真正区分它们的是可执行性,语义和要实现或使用的代码量。虽然我们不能给出一套确定性的规则来选择一种风格,但我们当然可以提供一些提示来帮助你做出决定。

相比于第一个例子,即观察者模式而言,回调函数在支持不同类型的事件时有一些限制。但是事实上,我们仍然可以通过将事件类型作为回调的参数传递,或者通过接受多个回调来区分多个事件。然而,这样做的话不能被认为是一个优雅的 API 。在这种情况下, EventEmitter 可以提供更好的接口和更精简的代码。

EventEmitter 更优秀的另一种应用场景是多次触发同一事件或不触发事件的情况。事实上,无论操作是否成功,一个回调预计都只会被调用一次。但有一种特殊情况是,我们可能不知道事件在哪个时间点触发,在这种情况下, EventEmitter 是首选。

最后,使用回调的 API 仅通知特定的回调,但是使用 EventEmitter 函数可以让多个监听器都接收到通知。

回调机制和事件机制结合使用

还有一些情况可以将事件机制和回调结合使用。特别是当我们导出异步函数时,这种模式非常有用。 node-glob 模块 是该模块的一个示例。

glob(pattern, [options], callback)

该函数将一个文件名匹配模式作为第一个参数,后面两个参数分别为一组选项和一个回调函数,对于匹配到指定文件名匹配模式的文件列表,相关回调函数会被调用。同时,该函数返回 EventEmitter ,它展现了当前进程的状态。例如,当成功匹配文件名时可以实时发布 match 事件,当文件列表全部匹配完毕时可以实时发布 end 事件,或者该进程被手动中止时发布 abort 事件。看以下代码:

const glob = require('glob');
glob('data/*.txt', (error, files) => console.log(`All files found: ${JSON.stringify(files)}`))
  .on('match', match => console.log(`Match found: ${match}`));

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文