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.all和Promise.race的区别?——all 等全部完成(有并发上限的需求可配合分批),race 只取最快完成的一个。
写在最后
异步编程的进化路线:回调 → Promise → async/await,理解这条线,前端面试的异步题基本就稳了。实际开发里记住一句话:能用 async/await 就别写嵌套回调,能用 Promise.all 就别串行等待。
把异步、事件循环这类高频考点留在 可面猫笔试助手 里多刷几遍,配合项目里的真实调用,理解才会真正落地。