JavaScript 中的异步操作

2025-06-07

JavaScript 中的异步操作

同步任务/程序是指每条指令逐步执行的任务/程序,每条指令都会阻塞处理器,直到执行完成为止。而异步任务/程序则不会阻塞处理器,而是并行执行任务,或者使用一种机制使其看起来像是并行执行。为了实现并行性,大多数编程语言都使用线程的概念。主线程会生成其他线程来执行某些工作,这样主程序就不会被阻塞。

JavaScript 是一种同步语言,它是单线程的。JavaScript 借助事件循环实现并行性。事件循环的工作原理令人惊叹,但超出了本文的讨论范围。我建议您观看Philip Roberts 的这次演讲。他以有趣的方式深入解释了这一点。简单来说,事件循环使我们的程序中的不同任务看起来像是并行执行的,但事实并非如此。异步代码看起来不同,行为也与同步代码不同。如果不小心,您可能会遇到诸如使用未定义值而不是异步操作的实际值等问题。

为什么不直接采用同步方式?

如果我们只使用同步操作,那么我们的程序和 UI 在操作过程中就会无响应。想象一下,如果你无法在每次 API 调用时与页面交互,所有网站都会感觉卡顿,你会感到烦躁。如果你的程序正在处理一些 CPU 密集型任务,其他任务也必须等待。这可不是什么好主意。
输入输出操作、网络调用就是异步操作的几个例子。

处理异步操作

有不同的机制可以帮助您处理异步操作,让我们来了解一下。

回调

回调函数是一个在异步操作完成时执行的函数。你将回调函数传递给异步操作,异步操作会在执行完成后调用回调函数。
我们以读取文件为例。为此,我们将使用Nodejs 中的 fs 模块

const fs = require('fs') // syntax to load a module
fs.readFile('/file-path', (err, data) => {
  if (err) console.log(err)
  console.log(data)
})

对于大多数操作的回调,需要注意的是它们的函数签名。约定是,如果发生错误,则第一个参数为错误对象;否则为 null/undefined;第二个参数为操作的结果。

虽然回调帮助我们处理异步操作,但它给我们带来了另一个问题,即回调地狱。

回调地狱

假设你有一个 file1,它的名字和你想从 file2 中读取数据的文件名相同。这种文件场景确实有点奇怪,但在处理 API 调用时,这种情况很常见,因为你需要根据第一个 API 的结果调用第二个 API。

const fs = require('fs') // syntax to load a module
fs.readFile('/file1', 'utf8', (err, file2) => {
  if (err) console.log(err)
  fs.readFile(`${file2}`, (err2, data) => {
    if (err) console.log(err2)
    console.log(data)
  })
})

如果你需要执行其他操作,则需要在另一个回调中嵌套一个回调,然后添加更多回调等等。代码变得难以查看和调试。随着代码库的增长,这会导致错误和维护问题。

承诺

Promise是回调的替代方案,要使用它,你需要一个返回 Promise 对象的函数。Promise 可以处于 resolve(成功)或 rejection(发生错误)状态,也可以处于 pending(待处理)状态。

这使得语法更加简单

readFile('file-path')
  .then(res => {})
  .catch(err => {})

.then().catch()也返回一个承诺,因此您可以在其中进行另一个异步操作,而不必经历回调地狱。

.catch() 块可帮助您处理 .then() 链中发生的任何错误。

有少数操作仍然不支持承诺,截至撰写本文时,fs 模块还不支持承诺,为了解决这个问题,您最好围绕它编写一个包装器,使用fs-extra npm 包,或者如果您使用的是节点 8 及以上版本,请使用util.promisify()方法。

const util = require('util')
const fs = require('fs')

const readFile = util.promisify(fs.readFile)

readFile('./test.txt', 'utf8')
  .then(res => {
    console.log('File data ', res)
  })
  .catch(err => {
    console.log(err)
  })

异步/等待

虽然 Promise 简化了我们的生活,但Async await让它变得更加轻松。async-await 内部使用 Promise 和Generators ,这是一种不同的语法。
其语法非常简洁,以至于异步代码看起来像同步的。这非常了不起,因为调试变得更加简单。使用它有两个步骤:在异步函数前使用 async 关键字;使用 await 关键字等待异步函数返回数据。await关键字只能在异步函数内部使用。

async function getData() {
  let data = await readFile("./test.txt", "utf8");
  console.log(data);
}
getData();

Async/Await 中的错误处理

由于 Async await 本身就是 Promise,因此您可以像普通 Promise 一样,在异步操作后直接使用 catch 代码块。如果您不喜欢这种方式,也可以使用传统的 try-catch 代码块。

如果您刚开始使用 Node,我希望本文能对您有所帮助。在后续的文章中,我们将探讨异步代码在处理 API 调用时的作用。如果您在异步操作结果方面遇到问题,请检查您是如何处理的。

如果您喜欢,请分享该帖子。

封面照片由 Héctor J. Rivas 在 Unsplash 上拍摄

文章来源:https://dev.to/kartik2406/async-operations-in-javascript-1k2c
PREV
从 Stellar 开始:从头开始构建全栈 dApp 所需的唯一教程。
NEXT
为什么每个开发人员都应该使用 TDD