>  기사  >  웹 프론트엔드  >  Node.js 이벤트 중심 메커니즘의 원리

Node.js 이벤트 중심 메커니즘의 원리

不言
不言원래의
2018-06-30 11:03:581759검색

이 글은 주로 Node.js의 이벤트 중심 메커니즘을 이해하는 원리를 소개합니다. 내용이 꽤 좋아서 지금 공유하고 참고하겠습니다.

Node.js를 배울 때 반드시 이해해야 할 사항 중 하나입니다. 이 문서는 주로 EventEmitter의 사용과 일부 비동기 상황 처리에 대해 다루고 있습니다.

대부분의 Node.js 객체는 EventEmitter 모듈을 사용하여 일반적으로 사용되는 HTTP 요청, 응답 및 스트림과 같은 이벤트를 수신하고 응답합니다.

const EventEmitter = require('events');

이벤트 중심 메커니즘의 가장 간단한 형태는 fs.readFile과 같이 Node.js에서 매우 인기 있는 콜백 함수입니다. 콜백 함수 형태로 이벤트가 발생할 때마다 콜백이 실행됩니다.

이 가장 기본적인 방법을 먼저 살펴보겠습니다.

준비되면 전화주세요, Node!

오래 전에는 js에 Promise에 대한 기본 지원이 없었고 async/await는 먼 꿈일 뿐이었고 콜백 함수는 비동기 문제를 처리하는 가장 원시적인 방법이었습니다.

콜백은 본질적으로 다른 함수에 전달되는 함수입니다. JavaScript에서 함수는 콜백의 존재를 가능하게 하는 일급 객체입니다.

코드의 콜백은 비동기 호출을 의미하지 않는다는 점을 이해해야 합니다. 콜백은 동기식 또는 비동기식으로 호출될 수 있습니다.

예를 들어, 다음은 콜백 함수 cb를 받아들이고 조건부 판단을 통해 콜백 함수를 동기식 또는 비동기식으로 호출할 수 있는 호스트 함수 fileSize입니다.

function fileSize (fileName, cb) {
 if (typeof fileName !== 'string') {
  // Sync
  return cb(new TypeError('argument should be string')); 
 } 
 fs.stat(fileName, (err, stats) => {
  if (err) {  
   // Async
   return cb(err); 
   } 
   // Async
  cb(null, stats.size);
 });
}

이것은 실제로 반례입니다. 다음과 같이 작성하는 경우가 많습니다. 호스트 함수를 디자인할 때 항상 콜백을 동기적으로 사용하거나 항상 비동기적으로 사용하여 동일한 스타일을 사용하도록 노력해야 합니다.

콜백 스타일로 작성된 일반적인 비동기 Node 함수의 간단한 예를 살펴보겠습니다.

const readFileAsArray = function(file, cb) {
 fs.readFile(file, function(err, data) {
  if (err) {
   return cb(err);
  }
  const lines = data.toString().trim().split('\n');
  cb(null, lines);
 });
};

readFileAsArray 함수는 파일 경로와 콜백 함수라는 두 가지 매개변수를 허용합니다. 파일 내용을 읽고 이를 행 배열로 분할한 다음 배열을 콜백 함수에 대한 인수로 전달하여 콜백 함수를 호출합니다.

이제 동일한 디렉토리에 있는 파일 number.txt에 다음 내용이 포함되어 있다고 가정하여 사용 사례를 설계합니다.

10
11
12
13
14
15

파일에서 홀수 개수를 세어야 하는 경우 다음을 사용할 수 있습니다. 코드를 단순화하는 readFileAsArray:

readFileAsArray('./numbers.txt', (err, lines) => {
 if (err) throw err;
 const numbers = lines.map(Number);
 const oddNumbers = numbers.filter(n => n%2 === 1);
 console.log('Odd numbers count:', oddNumbers.length);
});

이 코드는 파일 내용을 문자열 배열로 읽고, 콜백 함수는 이를 숫자로 구문 분석하고 홀수의 수를 계산합니다.

이것은 가장 순수한 노드 콜백 스타일입니다. 콜백의 첫 번째 매개변수는 오류 우선순위 원칙을 따라야 하며 err은 비어 있을 수 있으며 콜백을 호스트 함수의 마지막 매개변수로 전달해야 합니다. 사용자가 가정을 할 수 있으므로 항상 이런 방식으로 함수를 설계해야 합니다. 호스트 함수가 콜백을 마지막 인수로 사용하고 콜백 함수가 null 오류 개체를 첫 번째 인수로 사용하도록 합니다.

현대 JavaScript에서 콜백 대체

현대 JavaScript에는 비동기 API에서 콜백을 대체하는 데 사용할 수 있는 Promise가 있습니다. 콜백 함수는 호스트 함수의 매개변수로 전달되어야 하며(여러 호스트 콜백이 중첩되어 콜백 지옥을 형성함) 오류와 성공은 여기서만 처리될 수 있습니다. Promise 객체를 사용하면 성공과 오류를 별도로 처리할 수 있으며 여러 비동기 이벤트를 연쇄 호출할 수도 있습니다.

readFileAsArray 함수가 Promise를 지원하는 경우 다음과 같이 사용할 수 있습니다.

readFileAsArray('./numbers.txt')
 .then(lines => {
  const numbers = lines.map(Number);
  const oddNumbers = numbers.filter(n => n%2 === 1);
  console.log('Odd numbers count:', oddNumbers.length);
 })
 .catch(console.error);

우리는 호스트 함수의 반환 값에 대해 함수를 호출하여 우리의 요구 사항을 처리합니다. 콜백 버전은 여기서 익명 함수에 전달됩니다. 오류를 처리하기 위해 결과에 .catch 호출을 추가합니다. 그러면 오류를 포착하고 오류가 발생할 때 이에 대한 액세스를 제공합니다.

Promise 객체는 이미 최신 JavaScript에서 지원되므로 호스트 함수에서 쉽게 사용할 수 있습니다. 다음은 readFileAsArray 함수의 Promise 지원 버전입니다(기존 콜백 함수 메서드 지원).

const readFileAsArray = function(file, cb = () => {}) {
 return new Promise((resolve, reject) => {
  fs.readFile(file, function(err, data) {
   if (err) {
    reject(err);
    return cb(err);
   }   
   const lines = data.toString().trim().split('\n');
   resolve(lines);
   cb(null, lines);
  });
 });
};

함수가 fs.readFile에 대한 비동기 호출을 래핑하는 Promise 객체를 반환하도록 만듭니다. Promise 객체는 해결 함수와 거부 함수라는 두 가지 매개변수를 노출합니다.

예외가 발생하면 콜백 함수에 오류를 전달하여 오류를 처리하거나 Promise의 거부 기능을 사용할 수도 있습니다. 처리를 위해 콜백 함수에 데이터를 넘길 때마다 Promise의 해결 함수를 사용할 수도 있습니다.

콜백과 Promise를 동시에 사용할 수 있는 경우, 우리가 해야 할 유일한 일은 이 콜백 매개변수의 기본값을 설정하여 실행을 방지하고 콜백 함수 매개변수가 없을 때 오류를 보고하는 것입니다. 통과했다. 이 예시에서는 간단한 기본 빈 함수가 사용됩니다: () => {}.

async/await를 통해 Promise를 사용하세요

当需要连续调用异步函数时,使用 Promise 会让你的代码更容易编写。不断的使用回调会让事情变得越来越复杂,最终陷入回调地狱。

Promise 的出现改善了一点,Generator 的出现又改善了一点。 处理异步问题的最新解决方式是使用 async 函数,它允许我们将异步代码视为同步代码,使其整体上更加可读。

以下是使用 async/await 版本的调用 readFileAsArray 的例子:

async function countOdd () {
 try {
  const lines = await readFileAsArray('./numbers');
  const numbers = lines.map(Number);
  const oddCount = numbers.filter(n => n%2 === 1).length;
  console.log('Odd numbers count:', oddCount);
 } catch(err) {
  console.error(err);
 }
}
countOdd();

首先,我们创建了一个 async 函数 —— 就是一个普通的函数声明之前,加了个 async 关键字。在 async 函数内部,我们调用了 readFileAsArray 函数,就像把它的返回值赋值给变量 lines 一样,为了真的拿到 readFileAsArray 处理生成的行数组,我们使用关键字 await。之后,我们继续执行代码,就好像 readFileAsArray 的调用是同步的一样。

要让代码运行,我们可以直接调用 async 函数。这让我们的代码变得更加简单和易读。为了处理异常,我们需要将异步调用包装在一个 try/catch 语句中。

有了 async/await 这个特性,我们不必使用任何特殊的API(如 .then 和 .catch )。我们只是把这种函数标记出来,然后使用纯粹的 JavaScript 写代码。

我们可以把 async/await 这个特性用在支持使用 Promise 处理后续逻辑的函数上。但是,它无法用在只支持回调的异步函数上(例如setTimeout)。

EventEmitter 模块

EventEmitter 是一个处理 Node 中各个对象之间通信的模块。 EventEmitter 是 Node 异步事件驱动架构的核心。 Node 的许多内置模块都继承自 EventEmitter。

它的概念其实很简单:emitter 对象会发出被定义过的事件,导致之前注册的所有监听该事件的函数被调用。所以,emitter 对象基本上有两个主要特征:

  • 触发定义过的事件

  • 注册或者取消注册监听函数

为了使用 EventEmitter,我们需要创建一个继承自 EventEmitter 的类。

class MyEmitter extends EventEmitter {
}

我们从 EventEmitter 的子类实例化的对象,就是 emitter 对象:

const myEmitter = new MyEmitter();

在这些 emitter 对象的生命周期里,我们可以调用 emit 函数来触发我们想要的触发的任何被命名过的事件。

myEmitter.emit('something-happened');
emit 函数的使用表示发生某种情况发生了,让大家去做该做的事情。 这种情况通常是某些状态变化引起的。

我们可以使用 on 方法添加监听器函数,并且每次 emitter 对象触发其关联的事件时,将执行这些监听器函数。

事件 !== 异步

先看看这个例子:

const EventEmitter = require('events');

class WithLog extends EventEmitter {
 execute(taskFunc) {
  console.log('Before executing');
  this.emit('begin');
  taskFunc();
  this.emit('end');
  console.log('After executing');
 }
}

const withLog = new WithLog();

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

withLog.execute(() => console.log('*** Executing task ***'));

WithLog 是一个事件触发器,它有一个方法 —— execute,该方法接受一个参数,即具体要处理的任务函数,并在其前后包裹 log 以输出其执行日志。

为了看到这里会以什么顺序执行,我们在两个命名的事件上都注册了监听器,最后执行一个简单的任务来触发事件。

下面是上面程序的输出结果:

Before executing
About to execute
*** Executing task ***
Done with execute
After executing

这里我想证实的是以上的输出都是同步发生的,这段代码里没有什么异步的成分。

  • 第一行输出了 "Before executing"

  • begin 事件被触发,输出 "About to execute"

  • 真正应该被执行的任务函数被调用,输出 " Executing task "

  • end 事件被触发,输出 "Done with execute"

  • 最后输出 "After executing"

就像普通的回调一样,不要以为事件意味着同步或异步代码。

跟之前的回调一样,不要一提到事件就认为它是异步的或者同步的,还要具体分析。

如果我们传递 taskFunc 是一个异步函数,会发生什么呢?

// ...

withLog.execute(() => {
 setImmediate(() => {
  console.log('*** Executing task ***')
 });
});

输出结果变成了这样:

Before executing
About to execute
Done with execute
After executing
*** Executing task ***

这样就有问题了,异步函数的调用导致 "Done with execute" 和 "After executing" 的输出并不准确。

要在异步函数完成后发出事件,我们需要将回调(或 Promise)与基于事件的通信相结合。 下面的例子说明了这一点。

使用事件而不是常规回调的一个好处是,我们可以通过定义多个监听器对相同的信号做出多个不同的反应。如果使用回调来完成这件事,我们要在单个回调中写更多的处理逻辑。事件是应用程序允许多个外部插件在应用程序核心之上构建功能的好办法。你可以把它们当成钩子来挂一些由于状态变化而引发执行的程序。

异步事件

我们把刚刚那些同步代码的示例改成异步的:

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

class WithTime extends EventEmitter {
 execute(asyncFunc, ...args) {
  this.emit('begin');
  console.time('execute');
  asyncFunc(...args, (err, data) => {
   if (err) {
    return this.emit('error', err);
   }

   this.emit('data', data);
   console.timeEnd('execute');
   this.emit('end');
  });
 }
}

const withTime = new WithTime();

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

withTime.execute(fs.readFile, __filename);

用 WithTime 类执行 asyncFunc 函数,并通过调用 console.time 和 console.timeEnd 报告该asyncFunc 所花费的时间。它在执行之前和之后都将以正确的顺序触发相应的事件,并且还会发出 error/data 事件作为处理异步调用的信号。

我们传递一个异步的 fs.readFile 函数来测试一下 withTime emitter。 我们现在可以直接通过监听 data 事件来处理读取到的文件数据,而不用把这套处理逻辑写到 fs.readFile 的回调函数中。

执行这段代码,我们以预期的顺序执行了一系列事件,并且得到异步函数的执行时间,这些是十分重要的。

About to execute
execute: 4.507ms
Done with execute

请注意,我们是将回调与事件触发器 emitter 相结合实现的这部分功能。 如果 asynFunc 支持Promise,我们可以使用 async/await 函数来做同样的事情:

class WithTime extends EventEmitter {
 async execute(asyncFunc, ...args) {
  this.emit('begin');
  try {
   console.time('execute');
   const data = await asyncFunc(...args);
   this.emit('data', data);
   console.timeEnd('execute');
   this.emit('end');
  } catch(err) {
   this.emit('error', err);
  }
 }
}

我认为这段代码比之前的回调风格的代码以及使用 .then/.catch 风格的代码更具可读性。async/await 让我们更加接近 JavaScript 语言本身(不必再使用 .then/.catch 这些 api)。

事件参数和错误

在之前的例子中,有两个事件被发出时还携带了别的参数。

error 事件被触发时会携带一个 error 对象。

this.emit('error', err);

data 事件被触发时会携带一个 data 对象。

this.emit('data', data);

我们可以在 emit 函数中不断的添加参数,当然第一个参数一定是事件的名称,除去第一个参数之外的所有参数都可以在该事件注册的监听器中使用。

例如,要处理 data 事件,我们注册的监听器函数将访问传递给 emit 函数的 data 参数,而这个 data 也正是由 asyncFunc 返回的数据。

withTime.on('data', (data) => {
 // do something with data
});

error 事件比较特殊。在我们基于回调的那个示例中,如果不使用监听器处理 error 事件,node 进程将会退出。

举个由于错误使用参数而造成程序崩溃的例子:

class WithTime extends EventEmitter {
 execute(asyncFunc, ...args) {
  console.time('execute');
  asyncFunc(...args, (err, data) => {
   if (err) {
    return this.emit('error', err); // Not Handled
   }

   console.timeEnd('execute');
  });
 }
}

const withTime = new WithTime();

withTime.execute(fs.readFile, ''); // BAD CALL
withTime.execute(fs.readFile, __filename);

第一次调用 execute 将会触发 error 事件,由于没有处理 error ,Node 程序随之崩溃:

events.js:163
   throw er; // Unhandled 'error' event
   ^
Error: ENOENT: no such file or directory, open ''

第二次执行调用将受到此崩溃的影响,并且可能根本不会被执行。

如果我们为这个 error 事件注册一个监听器函数来处理 error,结果将大不相同:

withTime.on('error', (err) => {
 // do something with err, for example log it somewhere
 console.log(err)
});

如果我们执行上述操作,将会报告第一次执行 execute 时发送的错误,但是这次 node 进程不会崩溃退出,其他程序的调用也都能正常完成:

{ Error: ENOENT: no such file or directory, open '' errno: -2, code: 'ENOENT', syscall: 'open', path: '' }
execute: 4.276ms

需要注意的是,基于 Promise 的函数有些不同,它们暂时只是输出一个警告:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: ENOENT: no such file or directory, open ''

DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

另一种处理异常的方式是在监听全局的 uncaughtException 进程事件。 然而,使用该事件全局捕获错误并不是一个好办法。

关于 uncaughtException,一般都会建议你避免使用它,但是如果必须用它,你应该让进程退出:

process.on('uncaughtException', (err) => {
 // something went unhandled.
 // Do any cleanup and exit anyway!

 console.error(err); // don't do just that.

 // FORCE exit the process too.
 process.exit(1);
});

但是,假设在同一时间发生多个错误事件,这意味着上面的 uncaughtException 监听器将被多次触发,这可能会引起一些问题。

EventEmitter 模块暴露了 once 方法,这个方法发出的信号只会调用一次监听器。所以,这个方法常与 uncaughtException 一起使用。

监听器的顺序

如果针对一个事件注册多个监听器函数,当事件被触发时,这些监听器函数将按其注册的顺序被触发。

// first
withTime.on('data', (data) => {
 console.log(`Length: ${data.length}`);
});

// second
withTime.on('data', (data) => {
 console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

上述代码会先输出 Length 信息,再输出 Characters 信息,执行的顺序与注册的顺序保持一致。

如果你想定义一个新的监听函数,但是希望它能够第一个被执行,你还可以使用 prependListener 方法:

withTime.on('data', (data) => {
 console.log(`Length: ${data.length}`);
});

withTime.prependListener('data', (data) => {
 console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

上述代码中,Charaters 信息将首先被输出。

最后,你可以用 removeListener 函数来删除某个监听器函数。

以上就是本文的全部内容,希望对大家的学习有所帮助,更多相关内容请关注PHP中文网!

相关推荐:

node.js 利用流实现读写同步,边读边写的功能

关于nodejs socket服务端和客户端的简单通信功能

如何在NodeJS项目中使用ES6

위 내용은 Node.js 이벤트 중심 메커니즘의 원리의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.