在进行 JavaScript 应用开发时,您可能会接触到诸如浏览器中的 `fetch` 函数或者 Node.js 中的 `readFile` 函数这样的异步函数。
如果像处理普通函数一样使用这些函数,很可能会得到意料之外的结果,原因在于它们是异步执行的。本文将阐述异步函数的含义,并指导您如何像专业人士一样高效地使用它们。
同步函数概述
JavaScript 是一种单线程语言,这意味着它在同一时刻只能执行一项任务。 因此,当处理器遇到耗时的函数时,JavaScript 会暂停执行后续代码,直到该函数完全执行完毕。
大多数函数的操作完全依赖于处理器。 这意味着,在执行这些函数期间,无论耗时多久,处理器都处于忙碌状态。这类函数被称为同步函数。以下代码展示了一个同步函数的示例:
function add(a, b) { for (let i = 0; i < 1000000; i ++) { // 空操作 } return a + b; } // 调用函数需要一段时间 sum = add(10, 5); // 但处理器必须等待,才能执行后续代码 console.log(sum);
该函数执行一个循环次数很多的循环,需要一段时间才能返回两个输入参数的和。
定义此函数后,我们调用它,并将返回的结果储存在 `sum` 变量中,然后输出 `sum` 变量的值。 尽管执行 `add` 函数需要时间,但处理器必须等待该函数完成,才能继续输出结果。
您遇到的大多数函数都会以这种可预测的方式运作。 但是,某些函数是异步的,它们的行为方式与普通函数有所不同。
异步函数简介
异步函数将大部分工作转移到处理器之外执行。 这意味着,即使函数执行可能需要一些时间,处理器也会保持空闲,可以处理其他任务。
以下是一个异步函数的示例:
fetch('https://jsonplaceholder.typicode.com/users/1');
为了提高效率,JavaScript 允许处理器在异步函数完成执行之前继续执行其他需要 CPU 的任务。
由于处理器在异步函数执行完成之前就已经继续执行后续代码,因此异步函数的结果不会立即生效,而是处于待定状态。如果程序后续部分依赖于尚未完成的异步函数的结果,则会产生错误。
因此,处理器应该只执行那些不依赖于待定结果的代码。为了解决这个问题,现代 JavaScript 引入了 Promise。
JavaScript 中的 Promise 是什么?
在 JavaScript 中,Promise 是异步函数返回的一个表示未来值的占位符。 Promise 是现代 JavaScript 中异步编程的基础。
当一个 Promise 被创建后,会发生两种情况之一:如果异步函数成功返回,Promise 将会“解决”(resolved);如果发生错误,Promise 将会“拒绝”(rejected)。这些是 Promise 生命周期中的事件。我们可以为 Promise 添加事件处理程序,以便在 Promise 解决或拒绝时调用。
所有需要异步函数最终值的代码都可以附加到 Promise 的事件处理程序中,当 Promise 解决时,这些代码会被执行。所有处理被拒绝的 Promise 错误的代码也都将被附加到其相应的事件处理程序中。
以下是一个使用 Node.js 从文件中读取数据的示例:
const fs = require('fs/promises'); fileReadPromise = fs.readFile('./hello.txt', 'utf-8'); fileReadPromise.then((data) => console.log(data)); fileReadPromise.catch((error) => console.log(error));
第一行引入 `fs/promises` 模块。
第二行,我们调用 `readFile` 函数,传入文件名和编码方式。 该函数是异步的,所以它返回一个 Promise。 我们将这个 Promise 存储在 `fileReadPromise` 变量中。
第三行,我们添加了一个事件监听器,监听 Promise 何时解决。 我们通过调用 Promise 对象的 `then` 方法来实现这一点。 作为 `then` 方法的参数,我们传入了一个函数,当 Promise 解决时,该函数会被执行。
第四行,我们添加了一个监听器,监听 Promise 何时被拒绝。 这是通过调用 `catch` 方法并传递错误处理函数作为参数来完成的。
另一种方法是使用 `async` 和 `await` 关键字。 接下来我们将介绍这种方法。
`async` 和 `await` 的解析
`async` 和 `await` 关键字可以帮助我们以更易读和更易于理解的方式编写异步 JavaScript 代码。 本节将介绍如何使用这些关键字,以及它们对代码执行的影响。
`await` 关键字用于在等待异步函数完成时暂停函数的执行。 例如:
const fs = require('fs/promises'); async function readData() { const data = await fs.readFile('./hello.txt', 'utf-8'); // 只有当数据可用后,此行才会执行 console.log(data); } readData()
我们在调用 `readFile` 时使用了 `await` 关键字。 这告诉处理器等待文件读取完毕后才能执行下一行代码(`console.log`)。 这确保了只有在异步函数返回结果后,才会执行依赖于该结果的代码。
如果您尝试直接运行上面的代码,会遇到错误。 因为 `await` 只能在异步函数内部使用。 要将一个函数声明为异步函数,请在函数声明之前使用 `async` 关键字,如下所示:
const fs = require('fs/promises'); async function readData() { const data = await fs.readFile('./hello.txt', 'utf-8'); // 只有当数据可用后,此行才会执行 console.log(data); } // 调用函数使其运行 readData() // 此时代码将继续运行,同时等待 readData 函数完成 console.log('等待数据读取完成')
运行这段代码,您会看到 JavaScript 执行外部的 `console.log`,同时等待从文本文件中读取的数据。 一旦数据可用,就会执行 `readData` 函数中的 `console.log`。
使用 `async` 和 `await` 关键字进行错误处理通常采用 `try…catch` 结构。 了解如何在 `循环中使用 async/await` 也非常重要。
`async` 和 `await` 是现代 JavaScript 的特性。 传统上,异步代码是通过回调函数来编写的。
回调函数简介
回调函数是指当异步操作的结果可用时被调用的函数。 所有需要使用返回值进行操作的代码都应该放在回调函数中。 回调函数之外的代码不依赖于异步操作的结果,因此可以自由执行。
以下是一个 Node.js 中读取文件的示例:
const fs = require("fs"); fs.readFile("./hello.txt", "utf-8", (err, data) => { // 所有依赖于异步操作结果的代码都放在回调函数中 if (err) console.log(err); else console.log(data); }); // 在此部分,我们可以执行所有不需要结果的任务 console.log("Hello from the program")
第一行引入了 `fs` 模块。 接下来,我们调用 `fs` 模块的 `readFile` 函数。 `readFile` 函数会从指定的文件中读取文本内容。 第一个参数指定文件路径,第二个参数指定文件编码格式。
`readFile` 函数从文件中异步读取文本。为此,它需要一个函数作为参数,这个函数就是回调函数,当数据读取完成时会被调用。
传递给回调函数的第一个参数是错误信息,如果在函数运行过程中发生错误,它将具有一个值; 如果没有错误,它将为 `undefined`。
传递给回调的第二个参数是从文件中读取的数据。 回调函数内的代码可以访问读取的文件数据。回调函数之外的代码不依赖于该数据,因此可以在等待文件数据时执行。
运行上面的代码,输出结果如下:
JavaScript 的关键特性
有一些关键特性会影响异步 JavaScript 的工作方式。 以下视频对此进行了很好的解释:
下面,我将简要概述两个重要特性。
#1. 单线程
与其他允许多线程编程的语言不同,JavaScript 只允许使用一个线程。 线程是逻辑上相互依赖的指令序列。 多线程允许程序在遇到阻塞操作时执行不同的线程。
然而,多线程会增加复杂性,使得理解使用多线程的程序更加困难。 这增加了代码引入错误的风险,并使调试变得更加困难。为了简单起见,JavaScript 被设计为单线程。作为一种单线程语言,它依赖于事件驱动机制来有效地处理阻塞操作。
#2. 事件驱动
JavaScript 也是事件驱动的。 这意味着在 JavaScript 程序的生命周期中会发生某些事件。 作为程序员,您可以将函数附加到这些事件,每当事件发生时,附加的函数就会被调用并执行。
某些事件可能是因为阻塞操作的结果可用而触发的。 在这种情况下,会使用结果调用相关联的函数。
编写异步 JavaScript 时要考虑的事项
在最后一节中,我将提及编写异步 JavaScript 时需要考虑的一些事项,包括浏览器支持、最佳实践和重要性。
浏览器支持
下表显示了不同浏览器对 Promise 的支持情况。
来源:caniuse.com
下表显示了不同浏览器对 `async` 关键字的支持情况。
来源: caniuse.com
最佳实践
- 优先使用 `async/await`,因为它能帮助您编写更清晰、更易于理解的代码。
- 在 `try/catch` 代码块中处理错误。
- 仅在需要等待函数结果时才使用 `await` 关键字。
异步代码的重要性
异步代码使您能够编写仅使用单线程却效率更高的程序。 这非常重要,因为 JavaScript 常用于构建需要进行大量异步操作的网站,例如网络请求和磁盘文件读写。 这种效率也使得像 NodeJS 这样的运行环境日益流行,成为应用程序服务器的首选运行时。
结语
本文篇幅较长,但我们深入探讨了异步函数与常规同步函数的区别。 我们还讨论了如何使用 Promise、`async/await` 关键字和回调函数来处理异步操作。
此外,我们还介绍了 JavaScript 的主要特性。在最后一节中,我们讨论了浏览器支持和最佳实践。
接下来,您可以了解更多关于 Node.js 常见面试问题的内容。