Node.js 的工作原理

已完成

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

任务的类型

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

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

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

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

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

显示 Node J S 如何使用事件驱动的体系结构的关系图,其中事件循环处理事件并返回回调。

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

同步操作与异步操作

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 之间的差异以及异步代码的不同语法非常重要。