在前端开发中,异步操作随处可见:发起 HTTP 请求、操作本地文件、加载图片、定时动画......如果你没弄懂 JavaScript 中异步代码的执行顺序,踩坑只是早晚的事。
这篇文章就带你从最基础的异步执行流程讲起,深入理解 Promise 和 async/await 的用法和区别,最后再聊聊一些真实项目里如何优雅控制异步任务。
一、别把 setTimeout 当成异步的代表
我们经常用 setTimeout
举例异步,其实它更多是模拟。来看个简单例子:
js
setTimeout(() => {
console.log('异步任务');
}, 10);
console.log('同步任务开始');
for (let i = 0; i < 3; i++) {
console.log(`循环 ${i}`);
}
输出顺序:
同步任务开始
循环 0
循环 1
循环 2
异步任务
为什么 setTimeout
被推迟了?
因为 JavaScript 采用事件循环机制:同步任务放在主线程上执行,异步任务(如定时器、网络请求)被注册后挂到任务队列中,等主线程空闲时再执行。
二、Promise 到底解决了什么问题?
在没有 Promise 之前,我们处理异步依赖逻辑时经常这么写:
js
setTimeout(() => {
console.log('第一个异步');
setTimeout(() => {
console.log('第二个异步');
}, 1000);
}, 1000);
层层嵌套,看着就头疼。这就是回调地狱。
为了解决这个问题,ES6 引入了 Promise
:
js
const p = new Promise((resolve) => {
console.log('开始异步任务...');
setTimeout(() => {
resolve('成功');
}, 1000);
});
p.then((res) => {
console.log('任务完成:', res);
});
输出顺序:
makefile
开始异步任务...
任务完成: 成功
Promise 的基本结构
js
const promise = new Promise((resolve, reject) => {
// 异步任务逻辑
resolve(结果); // 或 reject(错误)
});
promise.then(res => {
// 处理结果
}).catch(err => {
// 处理异常
});
你可以理解成:我先答应你这个事(promise),等完成了我再通知你(then) 。
三、async/await:把异步写成"看起来是同步的"
虽然 .then()
写起来比回调舒服多了,但当你有多个异步串行执行时,链式结构依然会变得繁琐:
js
loadUser()
.then(user => getProfile(user.id))
.then(profile => updateUI(profile))
.catch(err => console.error(err));
换成 async/await
,逻辑清晰很多:
js
async function init() {
try {
const user = await loadUser();
const profile = await getProfile(user.id);
updateUI(profile);
} catch (err) {
console.error(err);
}
}
init();
async/await 本质上还是基于 Promise
只是语法糖,帮你隐藏了 .then()
,让代码看起来像同步。
js
(async function () {
const result = await new Promise(resolve => {
setTimeout(() => resolve('任务完成'), 1000);
});
console.log(result);
})();
输出:
任务完成
四、别踩这些 async/await 的坑!
-
await 只能在 async 函数中使用
否则会报错:
SyntaxError: await is only valid in async functions
-
await 会阻塞当前 async 函数,但不会阻塞主线程
意思是你函数后面那几行不会继续执行,直到这个 Promise 完成。
-
多个异步任务并不适合串行执行
如果几个请求可以并发,那就用
Promise.all()
:jsconst [res1, res2] = await Promise.all([ fetch('/api/a'), fetch('/api/b') ]);
五、真实案例:加载 GitHub 仓库列表
html
<ul id="repos"></ul>
<button id="btn">加载仓库</button>
<script>
document.getElementById('btn').addEventListener('click', async () => {
const res = await fetch('https://api.github.com/users/sleep202411/repos');
const data = await res.json();
document.getElementById('repos').innerHTML = data.map(item => `
<li><a href="${item.html_url}" target="_blank">${item.name}</a></li>
`).join('');
});
</script>
这段代码的执行流程:
- 用户点击按钮
- 发起请求(异步)
- 等待响应(await)
- 渲染到页面
看起来就像同步逻辑,这是 async/await 的最大魅力所在。
六、Node.js 场景:读取文件内容
用原始写法:
js
const fs = require('fs');
fs.readFile('./1.html', (err, data) => {
if (err) throw err;
console.log(data.toString());
});
Promise 化后:
js
const fs = require('fs');
const readFilePromise = new Promise((resolve, reject) => {
fs.readFile('./1.html', (err, data) => {
if (err) reject(err);
else {
console.log(data.toString());
resolve();
}
});
});
readFilePromise.then(() => {
console.log('文件读取完毕');
});
再用 async/await 简化:
js
const fs = require('fs/promises');
(async function () {
const content = await fs.readFile('./1.html', 'utf-8');
console.log(content);
})();
七、总结一下:Promise vs async/await 该怎么选?
场景 | 推荐用法 |
---|---|
简单异步流程 | Promise .then() |
多步骤串行依赖异步任务 | async/await |
多个异步任务并发执行 | Promise.all() |
错误处理要统一 | try/catch + await |
最后的提醒
很多前端同学对 async/await 的理解还停留在"可以简化代码"的阶段,但实际上,你必须清楚它的执行顺序、Promise 本质和异步队列的原理,才能在复杂项目中灵活使用。
希望这篇文章帮你把异步代码真正"搞明白",以后在调接口、加载数据、处理任务时不再乱用异步逻辑。