十年前我刚接触JavaScript时,回调地狱(Callback Hell)是每个前端开发者的噩梦。金字塔式的代码缩进让简单的业务逻辑变得难以维护,直到ES6的Promise和ES7的async/await出现才彻底改变了这一局面。但很多开发者(包括当年的我)常常混淆这两者的使用场景,比如在能使用简单Promise链的场景强行使用async函数,或是在需要精细控制异步流程时错误地选择了async/await。
Promise和async/await最本质的区别在于:Promise是异步操作的封装容器,而async/await是基于Promise的语法糖。这就好比Promise给了你一个可以装未来结果的盒子,而async/await则是让你像写同步代码一样操作这个盒子。但实际差异远不止于此——它们的错误处理机制、执行时机控制、调试体验等方面都存在关键差异。
Promise本质上是一个状态机,包含pending、fulfilled和rejected三种状态。这个状态转换是一次性的,一旦从pending变为其他状态就不可逆转。在Chrome控制台创建一个Promise实例时,你会看到[[PromiseState]]和[[PromiseResult]]两个内部属性,这正是其状态机特性的体现。
javascript复制const demoPromise = new Promise((resolve) => {
setTimeout(() => resolve('done'), 1000)
})
// 1秒后在控制台查看demoPromise
// [[PromiseState]]: "fulfilled"
// [[PromiseResult]]: "done"
Promise的.then()方法会产生一个新的Promise,这是链式调用的基础。但更关键的是,then回调会被放入微任务队列(Microtask Queue),这使其优先级高于setTimeout等宏任务。在事件循环中,微任务会在当前宏任务结束后立即执行,而宏任务要等到下次事件循环。
javascript复制console.log('脚本开始');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve()
.then(() => console.log('promise1'))
.then(() => console.log('promise2'));
console.log('脚本结束');
/* 输出顺序:
脚本开始
脚本结束
promise1
promise2
setTimeout
*/
Promise链具有"冒泡"特性——如果链中的某个Promise被reject且未被捕获,这个reject会一直向下传递,直到遇到.catch()。但一旦被捕获,后续的then会继续执行。这在复杂流程中需要特别注意:
javascript复制Promise.resolve()
.then(() => {
throw new Error('错误1');
})
.then(() => console.log('这里不会执行'))
.catch(err => console.log('捕获到:', err.message))
.then(() => console.log('这里会继续执行'));
// 输出:
// 捕获到: 错误1
// 这里会继续执行
async函数本质上是Generator函数的语法糖,底层通过协程(Coroutine)实现。当执行到await时,主线程会暂时交出控制权,等Promise settled后再恢复执行。这就像在电影院看电影时去上了个厕所——回来时会从离开的地方继续,而不是重头开始。
javascript复制async function fetchData() {
console.log('开始请求');
const res = await fetch('/api/data'); // 此处"暂停"
const data = await res.json(); // 上一个await解决后继续
return data;
}
任何async函数都会返回Promise,即使你返回的是非Promise值。这个特性常常被忽视:
javascript复制async function getNumber() {
return 42; // 等价于return Promise.resolve(42)
}
getNumber() instanceof Promise; // true
await的串行特性可能导致性能问题,但合理使用Promise.all可以优化:
javascript复制// 低效的串行请求
async function serialFetch() {
const res1 = await fetch('/api/1');
const res2 = await fetch('/api/2');
return [await res1.json(), await res2.json()];
}
// 改进的并行请求
async function parallelFetch() {
const [res1, res2] = await Promise.all([
fetch('/api/1'),
fetch('/api/2')
]);
return [await res1.json(), await res2.json()];
}
Promise使用.catch()方法,而async/await使用try/catch。在复杂场景下,两者的可读性差异明显:
javascript复制// Promise风格
function loadData() {
return fetchData()
.then(processData)
.catch(err => {
console.error('处理失败:', err);
return recoverData();
});
}
// async/await风格
async function loadData() {
try {
const data = await fetchData();
return processData(data);
} catch (err) {
console.error('处理失败:', err);
return recoverData();
}
}
Promise创建后会立即执行,而async函数需要被调用才会执行。这在模块加载等场景有重要影响:
javascript复制// Promise立即执行
const promise = new Promise(resolve => {
console.log('Promise执行');
resolve();
});
// async函数延迟执行
async function asyncFunc() {
console.log('async执行');
}
console.log('开始');
promise; // 会输出"Promise执行"
asyncFunc(); // 此时才会输出"async执行"
async/await的调试体验更接近同步代码,调用栈更清晰。在Chrome开发者工具中,async函数的await语句就像同步代码的断点,而Promise链的调试需要跟踪多个微任务。
Promise.race、Promise.allSettled等组合方法在async/await中同样适用:
javascript复制async function getFirstResponse(urls) {
const winner = await Promise.race(
urls.map(url => fetch(url).then(res => res.json()))
);
return winner;
}
原生Promise不支持取消,但可以通过AbortController实现:
javascript复制async function fetchWithTimeout(url, timeout) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求超时');
}
throw err;
}
}
未处理的Promise rejection可能导致内存泄漏。Node.js中可以通过以下方式监听:
javascript复制process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的rejection:', reason);
// 这里应该记录日志或进行其他处理
});
即使使用async/await,也可能出现类似回调地狱的嵌套:
javascript复制// 不推荐的深层嵌套
async function badPractice() {
const user = await getUser();
if (user) {
const posts = await getPosts(user.id);
if (posts.length) {
const comments = await getComments(posts[0].id);
// 更多嵌套...
}
}
}
// 改进后的扁平结构
async function betterPractice() {
const user = await getUser();
if (!user) return;
const posts = await getPosts(user.id);
if (!posts.length) return;
const comments = await getComments(posts[0].id);
// 继续处理...
}
不是所有异步操作都需要await,有时过早await会影响性能:
javascript复制// 不必要地串行化
async function slow() {
const a = await fetchA();
const b = await fetchB();
return { a, b };
}
// 更高效的并行版本
async function fast() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
return { a, b };
}
我的经验法则是:在async函数内部处理所有可预见的错误,只在最外层保留一个全局catch。对于库函数,应该让错误冒泡到调用方处理。同时,永远不要忽略Promise rejection——这会导致难以调试的静默失败。