Ezekielx
Ezekielx
发布于 2025-11-06 / 4 阅读
0
0

JavaScript 异步编程:理解 Promise 与 async/await,让异步代码更像同步代码

在 JavaScript 中,异步编程是一项基础而重要的能力。

不同于一般的编程操作,异步操作,例如网络请求、文件读取、数据库请求等,不会立即返回结果,需要我们对其进行恰当的处理,否则容易导致代码混乱、难以维护。

一、异步和同步

这里先解释异步和同步的关系。

**同步:**代码按顺执行,前面的没做完,后面的不会开始。

**异步:**某些操作可以 “先挂起”,主程序继续往下执行,等操作结束后再回来。

为什么要有异步?

在浏览器或 Node.js 中,有很多 “耗时操作”:

  • 网络请求
  • 文件读写(I/O)
  • 数据库查询
  • 计时器(setTimeout、setInterval)

如果都用同步方式执行,程序可能会 “卡住”,变得很慢。

但这里有两种情况:

无先后顺序的异步操作

访问淘宝官网时,有 3s 到 4s 的时间你会看到以下画面:

nWEfglst-1.png

可以看到在商品数据没有加载出来的时候,前端页面就已经渲染完毕了,加载前端页面和从数据库获取数据这就是两个异步操作。

显然,由于数据库获取商品数据的时间比较长,那么这一段空闲时间就完全可以用来加载前端数据。 如果是同步操作,那将变成在等待数据库获取商品时,你只能面对着浏览器的空白页面发呆🧐。

加载前端页面和从数据库获取数据是 “同时” 进行的。

有先后顺序的异步操作

当访问 B 站的创作中心时,它会提示你先登录,但输入账号密码点击登录按钮后,却没有立即跳转,而是出现登录中的字样。

nWEfglst-2.png

这很好理解,创作中心是和你的账号信息绑定的,在没有验证你的登录身份时,它是不知道应该加载什么信息的。

登录账号的加载创作中心页面是 “先后” 进行的。

这里主要讲有先后顺序的异步操作,毕竟无先后顺序的异步操作你直接把两异步函数放代码里就行了🤓。

二、Promise 出现之前

在早期的 JavaScript 中,没有 Promise,也没有 async/await,那么程序员是怎么执行异步操作的呢?

这时,异步任务通常通过 回调函数(Callback)来处理。

回调函数指的是:把一个函数作为参数传给另一个函数,在异步操作完成后再调用它。

这种模式的好处是灵活,但随着功能复杂,容易出现嵌套过多的问题。

例如,我们想实现以下逻辑:

  1. 请求用户信息。
  2. 用用户信息去获取文章。
  3. 打印文章结果。

使用回调函数:

// 模拟异步获取用户
function getUser(callback) {
  setTimeout(() => {
    console.log('用户数据已返回')
    callback({ id: 1, name: 'Ezekielx' })
  }, 1000)
}

// 模拟异步获取文章
function getPosts(user, callback) {
  setTimeout(() => {
    console.log(`已获取 ${user.name} 的文章`)
    callback([
      { id: 101, title: 'JavaScript 入门' },
      { id: 102, title: '理解 Promise' }
    ])
  }, 1000)
}

// 使用回调嵌套的方式获取结果
getUser(user => {
  getPosts(user, posts => {
    console.log('文章列表:', posts)
  })
})

返回结果:

用户数据已返回
已获取 Ezekielx 的文章
文章列表: [...]

这种方式的问题在于:当逻辑变复杂时(例如嵌套三层、四层回调),代码结构会急剧变得难以阅读。这种层层嵌套的现象被称为 回调地狱(Callback Hell)

这时你可能有疑问,为什么获取结果不写成:

// 不使用回调嵌套的方式获取结果
let user
let posts

getUser(data => {
  user = data
})

getPosts(user, data => {
  posts = data
})

console.log('用户:', user)
console.log('文章列表:', posts)

这就是典型的使用同步编程来写异步代码😡。如果这么写,输出结果将如下:

用户: undefined
文章列表: undefined
用户数据已返回
已获取 Ezekielx 的文章

为什么是空的结果是 undefined

因为 getUser()getPosts() 使用了 setTimeout() 模拟异步函数(相当于从数据库获取用户与文章信息等待的时间)。

执行顺序实际上是这样的:

  1. getUser() 被调用 → 启动异步任务 → 稍后执行
  2. getPosts() 被调用 → 启动异步任务 → 稍后执行
  3. console.log() 立即执行(此时异步任务还没完成)
  4. 一秒后,getUser 的回调才返回数据

所以当 console.log() 执行时,userposts 还没被赋值,自然就是 undefined

三、Promise 的出现

为了解决「回调地狱」的问题,ES6 引入了 Promise

Promise 是一个对象,表示一个「未来才会返回结果」的异步操作。

它通过链式调用 .then() 来处理异步结果,从而避免了层层嵌套。

// 返回 Promise 的异步函数
function getUser() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('用户数据已返回')
      resolve({ id: 1, name: 'Ezekielx' })
    }, 1000)
  })
}

function getPosts(user) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`已获取 ${user.name} 的文章`)
      resolve([
        { id: 101, title: 'JavaScript 入门' },
        { id: 102, title: '理解 Promise' }
      ])
    }, 1000)
  })
}

// 链式调用(比嵌套更清晰)
getUser()
  .then(user => getPosts(user))
  .then(posts => console.log('文章列表:', posts))
  .catch(err => console.error(err))

相比回调写法,Promise 让异步流程从“嵌套式”变为“顺序式”,可读性大幅提升。

不过,当异步流程很长时,.then() 链式结构依然稍显冗长,下面将给出现代的解决办法🥰。

四、Promise + async/await(现代写法)

ES8 引入的 async/await 语法,使得异步编程彻底摆脱了回调和链式的繁琐写法。

async 用于声明一个异步函数,

await 可以暂停函数执行,等待 Promise 完成后再继续

于是我们可以这样写:

async function showPosts() {
  try {
    const user = await getUser()       // 等待用户数据返回
    const posts = await getPosts(user) // 等待文章数据返回
    console.log('文章列表:', posts)
  } catch (err) {
    console.error('出错了:', err)
  }
}

showPosts()

这段代码逻辑非常直观:

  1. 获取用户
  2. 等待返回
  3. 再获取文章
  4. 打印结果

看起来就像同步执行一样,但实际上是等待上一个异步函数执行完毕后才执行的下一个异步函数(我知道有人认为这就是同步,但同步是不会等待的,他会一口气执行到底)。


评论