JavaScript 异步编程入门:从回调地狱到 Promise 与 async/await

从同步与异步的区别讲起,梳理回调、回调地狱、Promise 三种状态与链式调用,再到 async/await 的优雅写法,一篇讲清 JavaScript 异步编程的进化路线与核心概念。

JavaScript 是单线程语言,一次默认只能做一件事。但日常开发里网络请求、定时器、文件读取全是耗时操作——如果傻等它们完成,页面就卡死了。异步机制就是为了解决这个问题而生的。这篇从最基础的同步异步讲起,带你完整走一遍异步编程的进化路线。

一、同步与异步,到底差在哪

同步执行:按代码顺序从上到下依次执行,前一个执行完才轮到后一个。

异步执行:碰到耗时任务时不干等,先把精力让给其他任务,耗时任务完成后回调。听起来高效,但要小心依赖顺序问题:

let a = null
function A() { setTimeout(() => { a = 100 }, 1000) }
function B() { console.log(a) }

A()  // A 是耗时任务,先被挂起
B()  // 此时 a 还是 null,因为赋值还没执行

二、回调函数:最朴素的解法

既然 B 依赖 A 的结果,就把 B 的调用放进 A 完成后的回调里:

function A(callback) {
  setTimeout(() => { a = 100; callback() }, 1000)
}
A(() => console.log(a))  // 输出 100

但当业务复杂起来,回调一层套一层,就变成了著名的回调地狱:缩进越来越深、可读性差、排查困难。

三、Promise:把异步"状态化"

Promise 是一个代表"未来才会完成(或失败)"的对象,有三种状态:

  • pending(进行中):初始状态;
  • fulfilled(已成功):操作完成,调用 resolve;
  • rejected(已失败):操作失败,调用 reject。

用 Promise 改写依赖关系,把"回调嵌套"变成"链式调用":

function date() {
  return new Promise((resolve) => {
    setTimeout(() => { console.log('第一步完成'); resolve() }, 2000)
  })
}
function marry() {
  return new Promise((resolve) => {
    setTimeout(() => { console.log('第二步完成'); resolve() }, 1000)
  })
}
date().then(() => marry())

状态一旦改变就不可逆,所以 promise 的状态管理让异步流程变得可预测。配合 catch 统一捕获错误,比散落的回调清晰太多。

四、async/await:更优雅的最终形态

async/await 是 Promise 的语法糖,让异步代码长得像同步代码:

async function run() {
  await date()
  await marry()
  console.log('全部完成')
}
run()

要点:await 只能在 async 函数里用;await 会暂停当前函数执行,但不会阻塞主线程;错误处理用 try/catch 包裹。

五、异步面试常见追问

  • setTimeout(() => {}, 0) 真的立即执行吗?——不,要等当前同步代码和微任务队列清空;
  • Promise 和 setTimeout 谁先执行?——微任务(Promise.then)优先于宏任务(setTimeout),这也是事件循环的核心考点;
  • Promise.allPromise.race 的区别?——all 等全部完成(有并发上限的需求可配合分批),race 只取最快完成的一个。

写在最后

异步编程的进化路线:回调 → Promise → async/await,理解这条线,前端面试的异步题基本就稳了。实际开发里记住一句话:能用 async/await 就别写嵌套回调,能用 Promise.all 就别串行等待。

把异步、事件循环这类高频考点留在 可面猫笔试助手 里多刷几遍,配合项目里的真实调用,理解才会真正落地。