setTimeout、Promise、async/await 的区别
Category(分类): JavaScript Status: 已整理(2026)
原文只有题目和来源链接:setTimeout、Promise、Async Await 的区别。本文保留这个比较主题,并补充三者的执行模型、常见输出、错误处理和并发写法。
一、三者不是同一层面的东西
| 机制 | 主要作用 | 典型结果 |
|---|---|---|
setTimeout | 向宿主计时器注册一个稍后执行的回调 | 返回计时器句柄,回调进入任务队列 |
Promise | 表示异步操作的最终结果,并组合成功/失败链 | 返回 Promise 对象,处理器进入微任务队列 |
async/await | 以更接近同步代码的语法消费 Promise | async 函数始终返回 Promise |
setTimeout 是宿主提供的计时器 API;Promise 是 ECMAScript 的语言抽象;async/await 是建立在 Promise 之上的语言语法。它们经常一起使用,但不能互相替换。
二、setTimeout:至少等待一段时间后执行任务
console.log('start')
setTimeout(() => {
console.log('timer')
}, 0)
console.log('end')
输出一定先是:
start
end
timer 会在当前同步代码执行完、事件循环有机会处理该任务时运行。0 不是“立刻执行”,也不是严格的 0 毫秒保证;延迟时间只是一个不早于该时间的调度条件,实际还受当前任务、浏览器嵌套计时器规则、后台页面节流和 Node.js 事件循环影响。
const startedAt = Date.now()
setTimeout(() => {
console.log(`至少等待约 ${Date.now() - startedAt} 毫秒`)
}, 10)
计时器回调属于宿主任务(task/macrotask 的常用说法)。任务执行结束后,宿主通常会处理当前批次的微任务,然后再继续下一个任务;浏览器还会在合适时机进行渲染。
三、Promise:用微任务连接异步结果
Promise 构造器的 executor 是同步调用的:
console.log('before')
const promise = new Promise(resolve => {
console.log('executor')
setTimeout(() => resolve('resolved'), 0)
})
promise.then(value => console.log(value))
console.log('after')
输出顺序是:
before
executor
after
resolved
Promise 的 executor 同步执行,但 setTimeout 是异步的;then 的处理函数也不会同步执行。即使 Promise 已经 fulfilled,处理器仍会作为 Promise reaction 进入微任务队列:
const order = []
Promise.resolve().then(() => order.push('promise'))
order.push('sync')
console.log(order) // ['sync']
setTimeout(() => {
console.log(order) // ['sync', 'promise']
}, 0)
在浏览器中,当前任务结束后通常先清空微任务,再考虑下一个任务和渲染;不要把“微任务先于计时器”理解成所有宿主都完全相同的内部实现。Node.js 还存在 process.nextTick() 队列,其优先级和 Promise 微任务不同。
四、async/await:Promise 的语法层
async 函数总是返回 Promise:
async function getValue() {
return 42
}
console.log(getValue() instanceof Promise) // true
getValue().then(value => console.log(value)) // 42
await 会等待一个可被 Promise 解析的值,并暂停当前 async 函数的后续执行;它不会阻塞浏览器主线程,也不会让普通 JavaScript 代码停止运行:
function delay(milliseconds) {
return new Promise(resolve => {
setTimeout(resolve, milliseconds)
})
}
async function run() {
console.log('run start')
await delay(10)
console.log('run after await')
}
run()
console.log('outside')
// run start
// outside
// run after await
await 也可以等待普通值,效果类似于 await Promise.resolve(value):
async function example() {
const value = await 1
return value + 1
}
example().then(console.log) // 2
async/await 不会把异步操作变成同步操作,也不代表函数运行在新的线程中;它只是把 Promise 的后续逻辑写成了更容易阅读的形式。
五、三者放在一起时的执行顺序
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise then')
})
async function task() {
console.log('async start')
await null
console.log('async continuation')
}
task()
console.log('script end')
浏览器和常见 Node.js 环境中通常会看到:
script start
async start
script end
promise then
async continuation
setTimeout
解释:
- 顶层同步代码先运行;
- 调用
task()时,await之前的代码同步运行; Promise.then和await后续都排入微任务,通常按进入队列的先后执行;- 当前任务和微任务完成后,
setTimeout回调才有机会运行。
不能仅凭这段代码推断复杂浏览器渲染时机;渲染是宿主调度的一部分,Node.js 也有自己的事件循环阶段。
六、错误处理方式
6.1 setTimeout 中的异常不会被外层同步 try/catch 捕获
try {
setTimeout(() => {
throw new Error('timer error')
}, 0)
} catch (error) {
// 不会执行:异常发生在之后的另一个任务中
console.error(error)
}
需要在回调内部处理,或者把回调包装成 Promise:
function delayAndFail() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('timer error'))
}, 0)
})
}
delayAndFail().catch(error => {
console.error(error.message)
})
6.2 Promise 和 async/await 使用 catch 或 try/catch
async function loadData() {
try {
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return await response.json()
} catch (error) {
console.error('load failed:', error)
throw error
}
}
loadData().catch(error => {
console.error('caller handled:', error.message)
})
fetch 遇到 HTTP 404/500 时通常仍会 fulfilled,因此需要自行检查 response.ok;网络失败才会让 fetch Promise rejected。
七、顺序执行和并发执行
7.1 需要前一个结果时顺序等待
async function loadInOrder() {
const user = await fetch('/api/user').then(response => response.json())
const orders = await fetch(`/api/orders?user=${user.id}`)
.then(response => response.json())
return { user, orders }
}
7.2 互相独立的任务不要无意串行
下面的写法会等待第一个请求完成后才启动第二个请求:
async function loadSlowly() {
const user = await loadUser()
const settings = await loadSettings()
return { user, settings }
}
可以先创建两个 Promise,再使用 Promise.all 并发等待:
async function loadTogether() {
const userPromise = loadUser()
const settingsPromise = loadSettings()
const [user, settings] = await Promise.all([userPromise, settingsPromise])
return { user, settings }
}
Promise.all 只负责组合结果,不会自动取消失败后仍在进行的其他任务;网络请求需要 AbortController 等取消机制。
7.3 不要把 await 放进 forEach 期待顺序等待
// 常见错误:外层函数不会等待 forEach 中的 async 回调
items.forEach(async item => {
await save(item)
})
顺序处理使用 for...of:
async function saveInOrder(items) {
for (const item of items) {
await save(item)
}
}
需要并发处理并等待全部结果时使用 Promise.all:
async function saveTogether(items) {
await Promise.all(items.map(item => save(item)))
}
八、如何选择
- 只需要延迟或安排一次宿主任务:使用
setTimeout; - 需要表达异步结果、链式转换、错误传播或并发组合:使用 Promise;
- 需要以线性方式阅读 Promise 逻辑:使用
async/await; - 不要用
setTimeout(fn, 0)模拟 Promise 微任务或精确计时; - 不要把
async/await当作线程、阻塞式 I/O 或自动取消机制; - 生产代码需要同时设计超时、重试、取消、并发上限和错误边界。