异步任务是我们日常开发中离不开的一环,例如用户操作后的网络请求、动画延时回调、node.js 中各种异步 IO/进程操作等等。
过去通常是通过传递回调函数的形式使用,如今我们通常使用 Promise,配合 async/await,让日常这些异步处理方便了很多。
不过对于刚接触 Promise 的新同学来说,日常可能只接触和使用过其中比较基础的使用形式,又没有花时间去了解其中的实现原理,这就可能会导致一些错误理解和反模式实践。
这里将平时遇见过的问题列举出来,结合自己的理解,希望能帮新同学们绕开一些可以避免的坑。
个人认为,Promise 是一种 可链式触发的单向异步任务单元。
进行中
、已完成
、已拒绝
。进行中
,可 单向流转:进行中
→ 已完成
/已拒绝
;不可以逆向流转。进行中
状态下,可以通过它的 then(onResolved?, onRejected?)
函数指定 完成/拒绝状态回调函数。已完成
状态;已拒绝
状态。上面是 Promise 基本概念,看起来似乎“平平无奇”。然而它又通过以下机制实现了链式触发的效果:
then
函数中,将自动创建另一个临时 Promise 实例:已完成
状态。而其中 then
函数的状态回调函数还存在特殊情况:
then
的两个回调函数参数中,不存在对应当前 Promise 状态的回调函数时:这样,我们就可以在日常开发中通过 then
不断地链式创建临时 Promise,让我们的多个异步任务按照预期地逐个触发了。
async/await 被我们日常作为 Promise 状态回调函数函数的语法糖使用。
function createTask(factor) {
return new Promise((rs) => {
const start = new Date();
setTimeout(() => {
const end = new Date();
rs(end - start);
}, factor);
});
}
const work = () => {
console.log('Task start...');
const task = createTask(500);
task.then((cost) => {
console.log(`Task end. (${cost}ms)`);
}).catch((err) => {
console.error(err);
});
};
work();
上面的 work
可以使用 async/await 改写为:
const work = async () => {
console.log('Task start...');
try {
const task = createTask(500);
const cost = await task;
console.log(`Task end. (${cost}ms)`);
} catch (err) {
console.error(err);
}
};
只需要对 Promise 实例使用 await
操作符,就可以将异步任务的后续处理方式从嵌套的回调函数,彻底改变成仿佛是顺序执行的相同层级语句。甚至还可以使用 try/catch 同时捕获异步任务前后的异常。
尤其是对于多个异步任务逐个执行的情况,代码会简单和清晰很多,减轻业务开发中不必要的思维负担。
而对于暂时不支持 async/await 的浏览器环境,可以通过 babel+regeneratorRuntime 对项目代码进行转换,从而在日常开发中放心的使用这项新语法糖。
对于初次使用 Promise 的新手,可能会因为不知道可以在 then
回调内直接传递新的 Promise 作为 结果值,从而把 Promise 当作过去的回调函数使用,重新陷入回调地狱:
// Bad:
new Promise((rs) => {
console.log('Step 1 start...');
doSomething(() => {
console.log('Step 1 finished.');
rs();
});
}).then(() => {
new Promise((rs) => {
console.log('Step 2 start...');
doSomething(() => {
console.log('Step 2 finished.');
rs();
});
}).then(() => {
new Promise((rs) => {
console.log('Step 3 start...');
doSomething(() => {
console.log('Step 3 finished.');
rs();
});
}).then(() => {
console.log('All steps finished');
});
});
});
得益于 Promise 递归等待的机制,我们可以直接在最外层的 then
后面链式追加后续任务,并不需要反复嵌套:
new Promise((rs) => {
console.log('Step 1 start...');
doSomething(() => {
console.log('Step 1 finished.');
rs();
});
}).then(() => new Promise((rs) => {
console.log('Step 2 start...');
doSomething(() => {
console.log('Step 2 finished.');
rs();
});
})).then(() => new Promise((rs) => {
console.log('Step 3 start...');
doSomething(() => {
console.log('Step 3 finished.');
rs();
});
})).then(() => {
console.log('All steps finished');
});
当然,还可以使用 async/await 处理:
(async () => {
await new Promise((rs) => {
console.log('Step 1 start...');
doSomething(() => {
console.log('Step 1 finished.');
rs();
});
});
await new Promise((rs) => {
console.log('Step 2 start...');
doSomething(() => {
console.log('Step 2 finished.');
rs();
});
});
await new Promise((rs) => {
console.log('Step 3 start...');
doSomething(() => {
console.log('Step 3 finished.');
rs();
});
});
console.log('All steps finished');
})();
新同学使用日常使用 Promise 时,可能并不会留心给每次 Promise 调用的最后加上 catch()
进行异常捕获。
或者直接使用 try/catch
尝试捕获 Promise 异步任务和状态回调内的异常,发现没能如预期地捕获到。
这是由于 Promise 的异步函数执行时,已经脱离创建时的调用栈,其内部发生的错误没法直接被调用时的 try/catch
捕捉到。
可以通过以下例子模拟类似的情形:
function doItLater(fn, delay) {
setTimeout(fn, delay);
}
try {
doItLater(() => {
// 这个异常无法被这里的 try/catch 捕获到
throw new Error('Out of catch.');
}, 100);
} catch(ex) {
console.error(ex);
}
将
doItLater()
中的setTimeout(fn, delay)
改为fn()
同步调用,就能在外层捕获到异常。而 Promise 的状态回调并非同步执行,所以无法在外层直接捕获异常。
对于异步任务,我们需要通过 catch()
进行异常捕获,以便在外层做好任务被拒绝或者其它意外的处理:
new Promise((rs) => {
console.log('Task start...');
doSomething(() => {
console.log('Task finished.');
rs();
});
}).then(() => {
console.log('Done');
}).catch((ex) => {
console.error(ex);
reportError(ex);
});
不过 catch()
只能捕获到 Promise 内部的异常,如果需要同时捕获异步任务之前的某些同步处理异常,还得把相同的异常处理再用 try/catch
写一遍:
try {
doSomePreprocessing();
} catch (ex) {
// 异常处理
console.error(ex);
reportError(ex);
}
new Promise((rs) => {
try {
console.log('Task start...');
doSomething(() => {
console.log('Task finished.');
rs();
});
} catch (ex) {
// 异常处理
console.error(ex);
reportError(ex);
}
}).then(() => {
console.log('Done');
}).catch((ex) => {
// 异常处理
console.error(ex);
reportError(ex);
});
相同的异常处理写了三遍,有些可怕……不过上面的例子有点刻意了,doSomePreprocessing()
其实可以放在 Task start 相同的 try/catch 里。
但有时候也不一定能这样重新组织代码,不如直接使用 async/await
避免这样的冗余情况:
(async () => {
try {
doSomePreprocessing();
await new Promise((rs) => {
console.log('Task start...');
doSomething(() => {
console.log('Task finished.');
rs();
});
});
console.log('Done');
} catch (ex) {
// 异常处理
console.error(ex);
reportError(ex);
}
})();
日常开发中,如果涉及到多个异步任务的情况,新同学可能没有多想就直接使用 await
让它们逐个执行了:
(async () => {
// 展示 loading 动画
setLoading(true);
try {
// 加载商品类别信息
await loadGoodsCatalogs();
// 加载地区信息
await loadGeoData();
// 加载用户信息
await loadUserInfo();
// 加载用户绑定的收货地址
await loadUserAddress();
// 加载用户绑定的支付方式
await loadUserPayingMethods();
// 更新表单
refreshForm();
} catch (ex) {
showErrorInfo(ex);
}
// 关闭 loading 动画
setLoading(false);
})();
然而稍微观察就会发现,上面的请求的数据中可能存在前后依赖关系的情况,但也有不少可以并行处理的数据。
而让所有请求一股脑排队串行处理,既浪费现在日新月异的终端性能,又浪费用户宝贵的等待时间,未免有些暴殄天物。
对于并行处理的任务,我们可以使用 Promise.all()
方法:
让我们用它重新组织上面的异步任务,提高一下页面效率吧:
(async () => {
// 展示 loading 动画
setLoading(true);
try {
// 1. 需要逐个串行获取的用户相关数据
const loadUserData = async () => {
// 加载用户信息
await loadUserInfo();
// 加载用户绑定的收货地址
await loadUserAddress();
// 加载用户绑定的支付方式
await loadUserPayingMethods();
};
// 2. 可以并行处理的各类数据
await Promise.all([
// 加载商品类别信息
loadGoodsCatalogs(),
// 加载地区信息
loadGeoData(),
// 加载用户相关数据
loadUserData(),
]);
// 更新表单
refreshForm();
} catch (ex) {
showErrorInfo(ex);
}
// 关闭 loading 动画
setLoading(false);
})();
除了 Promise.all()
,还有两个类似的 Promise.race()
和 Promise.any()
方法。
Promise.race():
Promise.any():
注意! Promise.any() 方法依然是实验特性,尚未被浏览器完全支持。
对于类似 IO 任务的情况,可能需要反复确认完成进度的情况。
直接封装为只有开始结束态的 Promise 的话,会让用户长时间等待中无法获得任何感知,用户体验较差。
需要配合传统回调函数,结合具体的业务需求和页面交互进行实现。
在 Promise/A+ 的规范中,Promise 的实现可以是微任务,也可以是宏任务。不过普遍的共识一般将 Promise.then
的状态回调作为微任务实现。
相比之下,setTimeout
的宏任务将会在同一批创建的 Promise.then
微任务之后执行。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有