Node.js 的工作原理

已完成

本单元介绍 Node.js 如何处理 JavaScript 运行时的传入任务。

任务的类型

JavaScript 应用程序具有两种类型的任务:

  • 同步任务:这些任务将按顺序执行。 完成它们不依赖于其他资源。 例如数学运算或字符串操作。
  • 异步:这些任务可能不会立即完成,因为它们依赖于其他资源。 例如网络请求或文件系统操作。

由于希望程序尽可能快地运行,因此你会希望 JavaScript 引擎能够在等待异步操作的响应时继续工作。 为此,它将异步任务添加到任务队列,并继续处理下一个任务。

使用事件循环管理任务队列

Node.js 使用 JavaScript 引擎的事件驱动体系结构来处理异步请求。 下图大致说明了 V8 事件循环的工作原理:

Diagram showing how Node J S uses an event-driven architecture where an event loop processes events and returns callbacks.

向事件循环中添加了一个异步任务,该任务由适当的语法(如下所示)表示。 该任务包括要完成的工作和用于接收结果的回调函数。 完成密集型操作后,将触发回调函数并显示结果。

同步操作与异步操作

Node.js API 为某些操作(例如文件操作)同时提供了异步和同步操作。 虽然通常应该首先考虑使用异步操作,但有时可能会使用同步操作。

例如,当命令行接口 (CLI) 读取某个文件后立即使用该文件中的数据时。 在这种情况下,可以使用文件操作的同步版本,因为没有其他系统或人员在等待使用该应用程序。

但是,如果你要构建 Web 服务器,应始终使用文件操作的异步版本,以免阻止单个线程处理其他用户请求的执行能力。

作为 TailWind Traders 的开发人员,你需要了解同步操作和异步操作之间的区别,以及何时使用它们。

通过异步操作提高性能

Node.js 还会利用 JavaScript 独特的事件驱动特性,以快速高效地编写服务器任务。 当正确地与异步技术一起使用时,JavaScript 可以产生与低级语言(例如 C)相同的性能结果,因为 V8 引擎可以提高性能。

异步技术有三种样式,你需要能够在工作中识别它们:

  • Async/await(推荐):最新的异步技术,它使用 asyncawait 关键字接收异步操作的结果。 Async/await 用于许多编程语言中。 通常,具有较新依赖项的新项目将使用此异步代码样式。
  • 回调:原始异步技术,它使用回调函数接收异步操作的结果。 你在较旧的代码库和较旧的 Node.js API 中会看到此技术。
  • 承诺:一种较新的异步技术,它使用承诺对象接收异步操作的结果。 你在较新的代码库和较新的 Node.js API 中会看到此技术。 你可能需要在工作中编写基于承诺的代码,以包装不会更新的较旧 API。 通过使用承诺进行此包装,与较新的 async/await 样式的代码相比,你可以在更大范围的 Node.js 版本化项目中使用该代码。

Async/await

Async/await 是处理异步编程的最新方法。 Async/await 是基于承诺的语法糖衣,这使异步代码看起来更像同步代码。 它也更易于阅读和维护。

使用 async/await 的同一示例如下所示:

// async/await asynchronous example

const fs = require('fs').promises;

const filePath = './file.txt';

// `async` before the parent function
async function readFileAsync() {
  try {
    // `await` before the async method
    const data = await fs.readFile(filePath, 'utf-8');
    console.log(data);
    console.log('Done!');
  } catch (error) {
    console.log('An error occurred...: ', error);
  }
}

readFileAsync()
  .then(() => {
    console.log('Success!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

当在 ES2017 中发布 async/await 时,关键字只能在顶级函数为 Promise 的函数中使用。 虽然该 Promise 不必具有 thencatch 部分,但它仍需要有 promise 语法才能运行。

即使 async 函数内部没有 await 调用,它也始终返回一个 Promise。 该 Promise 将使用函数返回的值解析。 如果函数引发错误,则 Promise 将被拒绝,并且会返回抛出的值。

承诺

因为嵌套的回调可能难以读取和管理,所以 Node.js 添加了对承诺的支持。 承诺是表示异步操作最终完成(或失败)的对象。

承诺函数的格式为:

// Create a basic promise function
function promiseFunction() {
  return new Promise((resolve, reject) => {
    // do something

    if (error) {
      // indicate success
      reject(error);
    } else {
      // indicate error
      resolve(data);
    }
  });
}

// Call a basic promise function
promiseFunction()
  .then((data) => {
    // handle success
  })
  .catch((error) => {
    // handle error
  });

在履行承诺时调用 then 方法,并在拒绝承诺时调用 catch 方法。

若要使用承诺异步读取文件,则代码为:

// promises asynchronous example

const fs = require('fs').promises;
const filePath = './file.txt';

// request to read a file
fs.readFile(filePath, 'utf-8')
  .then((data) => {
    console.log(data);
    console.log('Done!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

console.log(`I'm the last line of the file!`);

顶级 async/await

最新版本的 Node.js 为 ES6 模块添加了顶级 async/await。 你需要在 package.json 中添加一个名为 type 且值为 module 的属性才能使用此功能。

{
    "type": "module"
}

然后,你可以在代码的顶层使用 await 关键字

// top-level async/await asynchronous example

const fs = require('fs').promises;

const filePath = './file.txt';

// `async` before the parent function
try {
  // `await` before the async method
  const data = await fs.readFile(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}
console.log("I'm the last line of the file!");

回调

最初发布 Node.js 时,异步编程是通过使用回叫函数来处理的。 回叫是作为参数传递给其他函数的函数。 任务完成后,将会调用回叫函数。

函数的参数顺序非常重要。 回调函数是函数的最后一个参数。

// Callback function is the last parameter
function(param1, param2, paramN, callback)

你在代码中维护的函数名称可能不叫作 callback。 它可能叫作 cbdonenext。 函数的名称并不重要,但参数的顺序很重要。

请注意,没有语法指示来表明函数是异步的。 你必须通过阅读文档或继续阅读代码才能知道函数是异步的。

具有已命名回调函数的回调示例

以下代码将异步函数与回调分开。 这易于阅读和理解,并允许你将该回调重复用于其他异步函数。

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// callback
const callback = (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
};

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function
fs.readFile(filePath, 'utf-8', callback);

console.log("I'm the last line of the file!");

正确的结果为:

I'm the last line of the file!
Hi, developers!
Done!

首先,异步函数 fs.readFile 被启动并进入事件循环。 然后,代码执行将继续执行下一个代码行,即最后的 console.log。 读取该文件后,将调用回调函数并执行两个 console.log 语句。

具有匿名函数的回调示例

以下示例使用一个匿名回调函数,这意味着该函数没有名称,不能被其他匿名函数重复使用。

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function () => {}
fs.readFile(filePath, 'utf-8', (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
});

console.log("I'm the last line of the file!");

正确的结果为:

I'm the last line of the file!
Hi, developers!
Done!

当执行该代码时,异步函数 fs.readFile 被启动并进入事件循环。 接下来,执行将继续执行以下代码行,即最后的 console.log。 读取该文件时,将调用回调函数并执行两个 console.log 语句。

嵌套的回调

因为你可能需要调用后续的异步回调,然后再调用另一个,所以回调代码可能会嵌套。 这被称为回调地狱,难以阅读和维护。

// nested callback example

// file system module from Node.js
const fs = require('fs');

fs.readFile(param1, param2, (error, data) => {
  if (!error) {
    fs.writeFile(paramsWrite, (error, data) => {
      if (!error) {
        fs.readFile(paramsRead, (error, data) => {
          if (!error) {
            // do something
          }
        });
      }
    });
  }
});

同步 API

Node.js 还具有一组同步 API。 在任务完成前,这些 API 会阻止程序执行。 如果要读取文件,然后立即使用该文件中的数据,同步 API 会非常有用。

Node.js 中的同步(阻塞)函数使用命名约定 functionSync。 例如,异步 readFile API 具有名为 readFileSync 的同步 API。 请务必在自己的项目中坚持遵循此标准,以便代码易于阅读和理解。

// synchronous example

const fs = require('fs');

const filePath = './file.txt';

try {
  // request to read a file
  const data = fs.readFileSync(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}

作为 TailWind Traders 的新开发人员,你可能会被要求修改任何类型的 Node.js 代码。 了解同步 API 和异步 API 之间的差异以及异步代码的不同语法非常重要。