异步陷阱:Promise、async/await 的迷惑行为
Promise和async/await是现代JavaScript异步编程的核心工具,但它们的某些行为常常让人感到困惑。从微任务队列的优先级到错误处理的隐蔽陷阱,这些特性背后隐藏着许多反直觉的细节。
Promise.resolve的"立即执行"假象
console.log('脚本开始');
Promise.resolve().then(() => console.log('Promise 1'));
setTimeout(() => console.log('setTimeout'), 0);
console.log('脚本结束');
// 输出顺序:
// 脚本开始
// 脚本结束
// Promise 1
// setTimeout
Promise回调会被放入微任务队列,而setTimeout回调进入宏任务队列。事件循环总是先清空微任务队列才会处理宏任务,即使setTimeout的延迟设置为0。这种优先级差异会导致出人意料的执行顺序。
更隐蔽的是,即使Promise已经处于resolved状态,它的then回调仍然会被异步执行:
const p = Promise.resolve(42);
p.then(v => console.log(v)); // 仍然异步执行
console.log('同步代码');
async函数的返回值陷阱
async函数总是返回Promise,但返回值处理有几个特殊场景:
async function foo() {
return 123; // 等价于return Promise.resolve(123)
}
async function bar() {
return Promise.resolve(456); // 注意这里会创建双重Promise
}
const result = bar();
result.then(v => console.log(v)); // 输出456,但经历了两次解包
当返回一个thenable对象时,行为会更加复杂:
async function baz() {
return {
then(resolve) {
resolve('手动thenable');
}
};
}
baz().then(console.log); // 输出"手动thenable"
错误处理的"黑洞"现象
未捕获的Promise拒绝会导致静默失败:
function riskyOperation() {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('失败')), 1000);
});
}
// 没有.catch处理
riskyOperation(); // 错误会被默默吞掉
在Node.js环境中,这可能会触发unhandledRejection事件。浏览器控制台通常会显示警告,但不会中断程序执行。
async/await的错误处理也有自己的特点:
async function fetchData() {
try {
const res = await fetch('invalid-url');
return await res.json();
} catch (e) {
console.log('捕获到错误:', e);
throw e; // 如果不重新抛出,外部不会知道发生了错误
}
}
// 调用方仍需处理错误
fetchData().catch(e => console.log('外部捕获:', e));
Promise链中的值穿透
then回调中返回非Promise值时会发生值穿透:
Promise.resolve(1)
.then(x => x + 1) // 返回2
.then(x => { /* 不返回值,相当于返回undefined */ })
.then(x => console.log(x)); // 输出undefined
但如果在then中返回Promise,则会等待该Promise解决:
Promise.resolve(1)
.then(x => new Promise(r => setTimeout(() => r(x + 1), 1000)))
.then(console.log); // 2 (1秒后)
async/await与并行执行
常见的误用是过度序列化异步操作:
// 低效写法
async function slowFetch() {
const a = await fetch('/api/a');
const b = await fetch('/api/b'); // 等待a完成才开始
return [a, b];
}
// 正确并行写法
async function fastFetch() {
const aPromise = fetch('/api/a');
const bPromise = fetch('/api/b');
return Promise.all([aPromise, bPromise]);
}
但并行执行时错误处理需要特别注意:
async function parallelWithError() {
try {
const [a, b] = await Promise.all([
fetch('/api/a'),
fetch('/api/b').then(() => Promise.reject('故意错误'))
]);
} catch (e) {
console.log('捕获到错误:', e); // 任何一个Promise拒绝都会触发
}
}
Promise构造函数的立即执行
Promise构造函数会立即执行执行器函数:
console.log('开始');
new Promise(resolve => {
console.log('执行器内部');
resolve();
});
console.log('结束');
// 输出顺序:
// 开始
// 执行器内部
// 结束
这个特性常被用来包装回调API:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(1000).then(() => console.log('1秒后'));
then回调的多次调用问题
一个Promise可以附加多个then回调:
const p = Promise.resolve('数据');
p.then(v => console.log('回调1:', v));
p.then(v => console.log('回调2:', v)); // 也会执行
但如果then回调中抛出错误,不会影响其他回调:
const p = Promise.resolve('安全');
p.then(() => { throw new Error('爆炸!') });
p.then(() => console.log('仍然执行')); // 这行会正常执行
async函数的隐式返回
没有return的async函数实际上返回一个resolved为undefined的Promise:
async function noReturn() {
// 没有return语句
}
noReturn().then(v => console.log(v)); // 输出undefined
即使函数内部抛出错误,返回的也是rejected Promise而非直接抛出:
async function throwsError() {
throw new Error('内部错误');
}
// 需要在调用处捕获
throwsError().catch(e => console.log('捕获:', e));
Promise.race的早期终止陷阱
Promise.race在第一个Promise敲定时就会返回,但其他Promise仍会继续执行:
function createTask(id, delay) {
return new Promise(resolve => {
setTimeout(() => {
console.log(`任务${id}完成`);
resolve(id);
}, delay);
});
}
Promise.race([
createTask(1, 1000),
createTask(2, 2000)
]).then(winner => console.log(`胜者: ${winner}`));
// 输出:
// 任务1完成
// 胜者: 1
// 任务2完成 (仍然会执行)
微任务队列的递归爆炸
微任务的递归添加会导致事件循环被阻塞:
function recursiveMicrotask(count = 0) {
if (count >= 100000) return;
Promise.resolve().then(() => {
console.log(`微任务 ${count}`);
recursiveMicrotask(count + 1);
});
}
recursiveMicrotask(); // 浏览器可能会卡死
相比之下,用setTimeout实现的递归是"友好"的:
function recursiveMacrotask(count = 0) {
if (count >= 100) return;
setTimeout(() => {
console.log(`宏任务 ${count}`);
recursiveMacrotask(count + 1);
}, 0);
}
recursiveMacrotask(); // 会分时执行
await的非Promise等待
await可以等待任何值,非Promise值会被自动包装:
async function awaitNonPromise() {
const v = await 42; // 等价于await Promise.resolve(42)
console.log(v); // 42
}
但等待thenable对象会有特殊行为:
async function awaitThenable() {
const v = await {
then(resolve) {
setTimeout(() => resolve('自定义then'), 100);
}
};
console.log(v); // '自定义then' (100ms后)
}
Promise.allSettled的特殊行为
与Promise.all不同,allSettled永远不会reject:
Promise.allSettled([
Promise.resolve('成功'),
Promise.reject('失败')
]).then(results => {
console.log(results);
// [
// { status: "fulfilled", value: "成功" },
// { status: "rejected", reason: "失败" }
// ]
});
async生成器函数的交互
async生成器函数结合了async和生成器特性:
async function* asyncGen() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
}
(async () => {
for await (const num of asyncGen()) {
console.log(num); // 1, 然后2
}
})();
这种模式在处理流数据时特别有用,但要注意错误传播方式与普通async函数不同。
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn