Node.js 的工作原理
本单元介绍 Node.js 如何处理 JavaScript 运行时的传入任务。
任务的类型
JavaScript 应用程序具有两种类型的任务:
- 同步任务:这些任务将按顺序执行。 完成它们不依赖于其他资源。 例如数学运算或字符串操作。
- 异步:这些任务可能不会立即完成,因为它们依赖于其他资源。 例如网络请求或文件系统操作。
由于希望程序尽可能快地运行,因此你会希望 JavaScript 引擎能够在等待异步操作的响应时继续工作。 为此,它将异步任务添加到任务队列,并继续处理下一个任务。
使用事件循环管理任务队列
Node.js 使用 JavaScript 引擎的事件驱动体系结构来处理异步请求。 下图大致说明了 V8 事件循环的工作原理:
向事件循环中添加了一个异步任务,该任务由适当的语法(如下所示)表示。 该任务包括要完成的工作和用于接收结果的回调函数。 完成密集型操作后,将触发回调函数并显示结果。
同步操作与异步操作
Node.js API 为某些操作(例如文件操作)同时提供了异步和同步操作。 虽然通常应该首先考虑使用异步操作,但有时可能会使用同步操作。
例如,当命令行接口 (CLI) 读取某个文件后立即使用该文件中的数据时。 在这种情况下,可以使用文件操作的同步版本,因为没有其他系统或人员在等待使用该应用程序。
但是,如果你要构建 Web 服务器,应始终使用文件操作的异步版本,以免阻止单个线程处理其他用户请求的执行能力。
作为 TailWind Traders 的开发人员,你需要了解同步操作和异步操作之间的区别,以及何时使用它们。
通过异步操作提高性能
Node.js 还会利用 JavaScript 独特的事件驱动特性,以快速高效地编写服务器任务。 当正确地与异步技术一起使用时,JavaScript 可以产生与低级语言(例如 C)相同的性能结果,因为 V8 引擎可以提高性能。
异步技术有三种样式,你需要能够在工作中识别它们:
- Async/await(推荐):最新的异步技术,它使用
async
和await
关键字接收异步操作的结果。 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 不必具有 then
和 catch
部分,但它仍需要有 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
。 它可能叫作 cb
、done
或 next
。 函数的名称并不重要,但参数的顺序很重要。
请注意,没有语法指示来表明函数是异步的。 你必须通过阅读文档或继续阅读代码才能知道函数是异步的。
具有已命名回调函数的回调示例
以下代码将异步函数与回调分开。 这易于阅读和理解,并允许你将该回调重复用于其他异步函数。
// 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 之间的差异以及异步代码的不同语法非常重要。