node.js事件驱动_了解Node.js事件驱动架构

node.js事件驱动

by Samer Buna

通过Samer Buna

了解Node.js事件驱动架构 (Understanding Node.js Event-Driven Architecture)

Update: This article is now part of my book “Node.js Beyond The Basics”.

更新:这篇文章现在是我的书《超越基础的Node.js》的一部分。

Read the updated version of this content and more about Node at jscomplete.com/node-beyond-basics.

jscomplete.com/node-beyond-basics中阅读此内容的更新版本以及有关Node的更多信息。

Most of Node’s objects — like HTTP requests, responses, and streams — implement the EventEmitter module so they can provide a way to emit and listen to events.

Node的大多数对象(例如HTTP请求,响应和流)都实现EventEmitter模块,因此它们可以提供一种发出和侦听事件的方式。

The simplest form of the event-driven nature is the callback style of some of the popular Node.js functions — for example, fs.readFile. In this analogy, the event will be fired once (when Node is ready to call the callback) and the callback acts as the event handler.

事件驱动性质的最简单形式是一些流行的Node.js函数的回调样式,例如fs.readFile 。 以此类推,事件将被触发一次(当Node准备好调用回调时),并且回调充当事件处理程序。

Let’s explore this basic form first.

让我们首先探讨这种基本形式。

准备好后给我打电话,Node! (Call me when you’re ready, Node!)

The original way Node handled asynchronous events was with callback. This was a long time ago, before JavaScript had native promises support and the async/await feature.

Node处理异步事件的原始方式是使用回调。 这是很久以前的事情,当时JavaScript还没有原生的Promise支持和异步/等待功能。

Callbacks are basically just functions that you pass to other functions. This is possible in JavaScript because functions are first class objects.

回调基本上只是传递给其他函数的函数。 在JavaScript中这是可能的,因为函数是第一类对象。

It’s important to understand that callbacks do not indicate an asynchronous call in the code. A function can call the callback both synchronously and asynchronously.

重要的是要了解回调不会在代码中指示异步调用。 函数可以同步和异步调用回调。

For example, here’s a host function fileSize that accepts a callback function cb and can invoke that callback function both synchronously and asynchronously based on a condition:

例如,下面是一个宿主函数fileSize ,它接受一个回调函数cb并可以根据条件同步和异步调用该回调函数:

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

Note that this is a bad practice that leads to unexpected errors. Design host functions to consume callback either always synchronously or always asynchronously.

请注意,这是一种不良做法,会导致意外错误。 设计主机函数以始终同步或始终异步使用回调。

Let’s explore a simple example of a typical asynchronous Node function that’s written with a callback style:

让我们研究一下用回调样式编写的典型异步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 takes a file path and a callback function. It reads the file content, splits it into an array of lines, and calls the callback function with that array.

readFileAsArray采用文件路径和回调函数。 它读取文件内容,将其拆分为一个行数组,然后使用该数组调用回调函数。

Here’s an example use for it. Assuming that we have the file numbers.txt in the same directory with content like this:

这是一个示例用法。 假设我们在同一目录中有文件numbers.txt ,其内容如下:

10
11
12
13
14
15

If we have a task to count the odd numbers in that file, we can use readFileAsArray to simplify the code:

如果我们有一个任务来计算该文件中的奇数,则可以使用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);
});

The code reads the numbers content into an array of strings, parses them as numbers, and counts the odd ones.

该代码将数字内容读入字符串数组,将其解析为数字,然后对奇数进行计数。

Node’s callback style is used purely here. The callback has an error-first argument err that’s nullable and we pass the callback as the last argument for the host function. You should always do that in your functions because users will probably assume that. Make the host function receive the callback as its last argument and make the callback expect an error object as its first argument.

纯粹在这里使用Node的回调样式。 回调函数的错误优先参数err可为空,我们将回调函数作为主机函数的最后一个参数传递。 您应该始终在函数中执行此操作,因为用户可能会假设这样做。 使主机函数将回调作为其最后一个参数接收,并使回调将错误对象作为其第一个参数。

替代回调的现代JavaScript (The modern JavaScript alternative to Callbacks)

In modern JavaScript, we have promise objects. Promises can be an alternative to callbacks for asynchronous APIs. Instead of passing a callback as an argument and handling the error in the same place, a promise object allows us to handle success and error cases separately and it also allows us to chain multiple asynchronous calls instead of nesting them.

在现代JavaScript中,我们有promise对象。 承诺可以替代异步API的回调。 Promise对象无需将回调作为参数传递并在同一位置处理错误,而是使我们可以分别处理成功和错误情况,还可以链接多个异步调用而不是嵌套它们。

If the readFileAsArray function supports promises, we can use it as follows:

如果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);

Instead of passing in a callback function, we called a .then function on the return value of the host function. This .then function usually gives us access to the same lines array that we get in the callback version, and we can do our processing on it as before. To handle errors, we add a .catch call on the result and that gives us access to an error when it happens.

我们没有传递回调函数,而是在宿主函数的返回值上调用了.then函数。 这个.then函数通常使我们能够访问与回调版本中相同的lines数组,并且我们可以像以前一样对其进行处理。 为了处理错误,我们在结果上添加了.catch调用,使我们可以在错误发生时对其进行访问。

Making the host function support a promise interface is easier in modern JavaScript thanks to the new Promise object. Here’s the readFileAsArray function modified to support a promise interface in addition to the callback interface it already supports:

由于有了新的Promise对象,在现代JavaScript中使宿主函数支持Promise接口更加容易。 这是已修改的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);
    });
  });
};

So we make the function return a Promise object, which wraps the fs.readFile async call. The promise object exposes two arguments, a resolve function and a reject function.

因此,我们使函数返回一个Promise对象,该对象包装了fs.readFile异步调用。 Promise对象公开两个参数,一个resolve函数和一个reject函数。

Whenever we want to invoke the callback with an error we use the promise reject function as well, and whenever we want to invoke the callback with data we use the promise resolve function as well.

每当我们想用错误调用回调函数时,我们也会使用promise reject函数,每当我们想对数据调用回调函数时,我们也将使用promise resolve函数。

The only other thing we needed to do in this case is to have a default value for this callback argument in case the code is being used with the promise interface. We can use a simple, default empty function in the argument for that case: () => {}.

在这种情况下,我们唯一需要做的另一件事就是为该回调参数设置一个默认值,以防代码与promise接口一起使用。 在这种情况下,我们可以在参数中使用一个简单的默认空函数: () => {}。

使用async / await消费诺言 (Consuming promises with async/await)

Adding a promise interface makes your code a lot easier to work with when there is a need to loop over an async function. With callbacks, things become messy.

当需要循环异步功能时,添加一个promise接口会使您的代码更容易使用。 使用回调,事情变得混乱。

Promises improve that a little bit, and function generators improve on that a little bit more. This said, a more recent alternative to working with async code is to use the async function, which allows us to treat async code as if it was synchronous, making it a lot more readable overall.

承诺会有所改善,函数生成器会有所改善。 这就是说,使用异步代码的另一种替代方法是使用async函数,该函数使我们可以将异步代码视为同步代码,从而使整体可读性更高。

Here’s how we can consume the readFileAsArray function with async/await:

这是我们如何在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();

We first create an async function, which is just a normal function with the word async before it. Inside the async function, we call the readFileAsArray function as if it returns the lines variable, and to make that work, we use the keyword await. After that, we continue the code as if the readFileAsArray call was synchronous.

我们首先创建一个异步函数,这只是一个普通函数,其前面带有单词async 。 在async函数内部,我们调用readFileAsArray函数,就好像它返回lines变量一样,为了使其正常工作,我们使用关键字await 。 之后,我们继续执行代码,就像readFileAsArray调用是同步的一样。

To get things to run, we execute the async function. This is very simple and more readable. To work with errors, we need to wrap the async call in a try/catch statement.

为了使事情运行,我们执行异步功能。 这非常简单并且可读性强。 要处理错误,我们需要将异步调用包装在try / catch语句中。

With this async/await feature, we did not have to use any special API (like .then and .catch). We just labeled functions differently and used pure JavaScript for the code.

使用此异步/等待功能,我们不必使用任何特殊的API(例如.then和.catch)。 我们只是对函数进行了不同的标记,并对代码使用了纯JavaScript。

We can use the async/await feature with any function that supports a promise interface. However, we can’t use it with callback-style async functions (like setTimeout for example).

我们可以将async / await功能与任何支持promise接口的功能一起使用。 但是,我们不能将其与回调样式的异步函数(例如setTimeout)一起使用。

EventEmitter模块 (The EventEmitter Module)

The EventEmitter is a module that facilitates communication between objects in Node. EventEmitter is at the core of Node asynchronous event-driven architecture. Many of Node’s built-in modules inherit from EventEmitter.

EventEmitter是一个模块,可促进Node中对象之间的通信。 EventEmitter是Node异步事件驱动的体系结构的核心。 Node的许多内置模块都继承自EventEmitter。

The concept is simple: emitter objects emit named events that cause previously registered listeners to be called. So, an emitter object basically has two main features:

这个概念很简单:发射器对象发出命名事件,这些事件导致先前注册的侦听器被调用。 因此,发射器对象基本上具有两个主要功能:

  • Emitting name events.

    发出名称事件。
  • Registering and unregistering listener functions.

    注册和注销侦听器功能。

To work with the EventEmitter, we just create a class that extends EventEmitter.

要使用EventEmitter,我们只需创建一个扩展EventEmitter的类。

class MyEmitter extends EventEmitter {}

Emitter objects are what we instantiate from the EventEmitter-based classes:

发射器对象是我们从基于EventEmitter的类中实例化的:

const myEmitter = new MyEmitter();

At any point in the lifecycle of those emitter objects, we can use the emit function to emit any named event we want.

在这些发射器对象的生命周期中的任何时候,我们都可以使用发出函数来发出我们想要的任何命名事件。

myEmitter.emit('something-happened');

Emitting an event is the signal that some condition has occurred. This condition is usually about a state change in the emitting object.

发出事件是已发生某种情况的信号。 该条件通常与发射物体的状态变化有关。

We can add listener functions using the on method, and those listener functions will be executed every time the emitter object emits their associated name event.

我们可以使用on方法添加侦听器函数,这些侦听器函数将在每次发射器对象发出其关联的名称事件时执行。

事件!==异步 (Events !== Asynchrony)

Let’s take a look at an example:

让我们看一个例子:

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 ***'));

Class WithLog is an event emitter. It defines one instance function execute. This execute function receives one argument, a task function, and wraps its execution with log statements. It fires events before and after the execution.

WithLog类是事件发射器。 它定义了一个实例函数execute 。 该execute函数接收一个参数,一个task函数,并用log语句包装其执行。 它在执行前后触发事件。

To see the sequence of what will happen here, we register listeners on both named events and finally execute a sample task to trigger things.

要查看此处发生的顺序,我们在两个命名事件上注册侦听器,最后执行一个示例任务来触发事件。

Here’s the output of that:

这是输出:

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

What I want you to notice about the output above is that it all happens synchronously. There is nothing asynchronous about this code.

我希望您注意到上面的输出,所有操作都是同步发生的。 此代码没有异步的。

  • We get the “Before executing” line first.

    我们首先得到“执行之前”这一行。
  • The begin named event then causes the “About to execute” line.

    然后, begin命名事件将导致“关于要执行”行。

  • The actual execution line then outputs the “*** Executing task ***” line.

    然后,实际执行行将输出“ ***执行任务***”行。
  • The end named event then causes the “Done with execute” line

    然后,以命名end事件导致“完成并执行”行

  • We get the “After executing” line last.

    我们最后得到“执行后”行。

Just like plain-old callbacks, do not assume that events mean synchronous or asynchronous code.

就像普通的回调一样,不要假定事件表示同步或异步代码。

This is important, because if we pass an asynchronous taskFunc to execute, the events emitted will no longer be accurate.

这很重要,因为如果我们传递异步taskFuncexecute ,则发出的事件将不再准确。

We can simulate the case with a setImmediate call:

我们可以使用setImmediate调用来模拟这种情况:

// ...

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

Now the output would be:

现在的输出将是:

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

This is wrong. The lines after the async call, which were caused the “Done with execute” and “After executing” calls, are not accurate any more.

错了 异步调用之后的行(导致“执行完成”和“执行后”调用)不再准确。

To emit an event after an asynchronous function is done, we’ll need to combine callbacks (or promises) with this event-based communication. The example below demonstrates that.

为了在异步函数完成后发出事件,我们需要将回调(或promise)与基于事件的通信结合起来。 下面的示例演示了这一点。

One benefit of using events instead of regular callbacks is that we can react to the same signal multiple times by defining multiple listeners. To accomplish the same with callbacks, we have to write more logic inside the single available callback. Events are a great way for applications to allow multiple external plugins to build functionality on top of the application’s core. You can think of them as hook points to allow for customizing the story around a state change.

使用事件而不是常规回调的好处之一是,我们可以通过定义多个侦听器来对同一信号进行多次响应。 为实现回调,我们必须在单个可用回调内编写更多逻辑。 事件是应用程序允许多个外部插件在应用程序核心之上构建功能的好方法。 您可以将它们视为挂钩点,以允许围绕状态更改自定义故事。

异步事件 (Asynchronous Events)

Let’s convert the synchronous sample example into something asynchronous and a little bit more useful.

让我们将同步示例示例转换为异步示例,然后再使用一些示例。

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);

The WithTime class executes an asyncFunc and reports the time that’s taken by that asyncFunc using console.time and console.timeEnd calls. It emits the right sequence of events before and after the execution. And also emits error/data events to work with the usual signals of asynchronous calls.

WithTime类执行的asyncFunc和报告,是采取由时间asyncFunc使用console.timeconsole.timeEnd电话。 它在执行前后发出正确的事件序列。 并且还会发出错误/数据事件以与异步调用的通常信号一起工作。

We test a withTime emitter by passing it an fs.readFile call, which is an asynchronous function. Instead of handling file data with a callback, we can now listen to the data event.

我们通过传递一个fs.readFile调用来测试withTime发射器,这是一个异步函数。 现在,我们可以侦听数据事件,而不是使用回调处理文件数据。

When we execute this code , we get the right sequence of events, as expected, and we get a reported time for the execution, which is helpful:

当我们执行此代码时,我们将按预期获得正确的事件序列,并获得执行的报告时间,这很有帮助:

About to execute
execute: 4.507ms
Done with execute

Note how we needed to combine a callback with an event emitter to accomplish that. If the asynFunc supported promises as well, we could use the async/await feature to do the same:

请注意,我们需要如何结合使用回调和事件发射器来实现这一点。 如果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);
    }
  }
}

I don’t know about you, but this is much more readable to me than the callback-based code or any .then/.catch lines. The async/await feature brings us as close as possible to the JavaScript language itself, which I think is a big win.

我不了解您,但是比起基于回调的代码或任何.then / .catch行,这对我而言更具可读性。 异步/等待功能使我们尽可能接近JavaScript语言本身,我认为这是一个巨大的胜利。

事件参数和错误 (Events Arguments and Errors)

In the previous example, there were two events that were emitted with extra arguments.

在前面的示例中,有两个带有额外参数的事件。

The error event is emitted with an error object.

错误事件与错误对象一起发出。

this.emit('error', err);

The data event is emitted with a data object.

数据事件与数据对象一起发出。

this.emit('data', data);

We can use as many arguments as we need after the named event, and all these arguments will be available inside the listener functions we register for these named events.

在命名事件之后,我们可以根据需要使用任意数量的参数,并且所有这些参数都将在我们为这些命名事件注册的侦听器函数中可用。

For example, to work with the data event, the listener function that we register will get access to the data argument that was passed to the emitted event and that data object is exactly what the asyncFunc exposes.

例如,要处理数据事件,我们注册的侦听器函数将可以访问传递给发出的事件的数据参数,而该数据对象正是asyncFunc公开的。

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

The error event is usually a special one. In our callback-based example, if we don’t handle the error event with a listener, the node process will actually exit.

error事件通常是一个特殊的事件。 在基于回调的示例中,如果不使用侦听器处理错误事件,则节点进程实际上将退出。

To demonstrate that, make another call to the execute method with a bad argument:

为了证明这一点,请使用错误的参数再次调用execute方法:

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);

The first execute call above will trigger an error. The node process is going to crash and exit:

上面的第一个execute调用将触发错误。 节点进程将崩溃并退出:

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

The second execute call will be affected by this crash and will potentially not get executed at all.

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

If we register a listener for the special error event, the behavior of the node process will change. For example:

如果我们为特殊error事件注册一个侦听器,则节点进程的行为将改变。 例如:

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

If we do the above, the error from the first execute call will be reported but the node process will not crash and exit. The other execute call will finish normally:

如果执行上述操作,将报告来自第一个执行调用的错误,但节点进程不会崩溃并退出。 另一个执行调用将正常完成:

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

Note that Node currently behaves differently with promise-based functions and just outputs a warning, but that will eventually change:

请注意,Node当前与基于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.

The other way to handle exceptions from emitted errors is to register a listener for the global uncaughtException process event. However, catching errors globally with that event is a bad idea.

处理来自发出的错误的异常的另一种方法是为全局uncaughtException流程事件注册一个侦听器。 但是,在该事件中全局捕获错误不是一个好主意。

The standard advice about uncaughtException is to avoid using it, but if you must do (say to report what happened or do cleanups), you should just let the process exit anyway:

关于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);
});

However, imagine that multiple error events happen at the exact same time. This means the uncaughtException listener above will be triggered multiple times, which might be a problem for some cleanup code. An example of this is when multiple calls are made to a database shutdown action.

但是,想象一下,多个错误事件恰好同时发生。 这意味着上面的uncaughtException侦听器将被多次触发,这对于某些清理代码可能是一个问题。 例如,当多次调用数据库关闭操作时。

The EventEmitter module exposes a once method. This method signals to invoke the listener just once, not every time it happens. So, this is a practical use case to use with the uncaughtException because with the first uncaught exception we’ll start doing the cleanup and we know that we’re going to exit the process anyway.

EventEmitter模块公开once方法。 此方法发出信号仅一次调用侦听器,而不是每次都调用。 因此,这是一个与uncaughtException一起使用的实际用例,因为对于第一个未捕获的异常,我们将开始进行清理,并且我们知道无论如何都将退出该过程。

听众顺序 (Order of Listeners)

If we register multiple listeners for the same event, the invocation of those listeners will be in order. The first listener that we register is the first listener that gets invoked.

如果我们为同一事件注册多个侦听器,则这些侦听器的调用将是有序的。 我们注册的第一个侦听器是被调用的第一个侦听器。

// प्रथम
withTime.on('data', (data) => {
  console.log(`Length: ${data.length}`);
});

// दूसरा
withTime.on('data', (data) => {
  console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

The above code will cause the “Length” line to be logged before the “Characters” line, because that’s the order in which we defined those listeners.

上面的代码将导致“长度”行记录在“字符”行之前,因为这是我们定义这些侦听器的顺序。

If you need to define a new listener, but have that listener invoked first, you can use the prependListener method:

如果您需要定义一个新的侦听器,但首先要调用该侦听器,则可以使用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);

The above will cause the “Characters” line to be logged first.

以上将导致“字符”行被首先记录。

And finally, if you need to remove a listener, you can use the removeListener method.

最后,如果需要删除侦听器,则可以使用removeListener方法。

That’s all I have for this topic. Thanks for reading! Until next time!

这就是我要做的所有事情。 谢谢阅读! 直到下一次!

Learning React or Node? Checkout my books:

学习React还是Node? 结帐我的书:

  • Learn React.js by Building Games

    通过构建游戏学习React.js

  • Node.js Beyond the Basics

    超越基础的Node.js

翻译自: https://www.freecodecamp.org/news/understanding-node-js-event-driven-architecture-223292fcbc2d/

node.js事件驱动

你可能感兴趣的:(python,javascript,java,vue,linux,ViewUI)