页面中的任务都运行在渲染进程的主线程上。当我们需要进行一项比较耗时的任务时(例如,从网络下载资源文件时),为了避免主线程被长时间占用产生卡顿,我们会采用异步回调的方式。将耗时任务放到其它的进程中执行,当进程处理完主线程分配的耗时任务后,该任务会被添加到消息队列中,供主线程触发回调。
凡事皆有代价,异步回调的影响体现在代码的编写上。例如,当需要依次加载若干脚本时:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...加载完所有脚本
});
// ...错误处理
});
// ...错误处理
});
在这个例子中,我们仅使用了简化的代码作为示例,就已经显得有些混乱了。而在实际开发中回调的复杂性要远高于这个示例。为了解决这个问题,先来看看代码混乱的原因是什么:
- 回调函数的嵌套调用。每个异步任务都需要先得到上一个异步任务处理的结果,而上一个异步任务的回调函数内部又会有新的独立内容。按此层层嵌套,代码杂糅在一起,可读性很差。
- 回调任务可能失败。回调任务并不能保证一定成功,因此在每一层回调中还需要处理上一个异步任务失败的情况,进一步增加了代码的复杂度。 因此,我们需要一种能够消除嵌套调用,且能够合并回调任务失败产生的错误的方法。使用Promise对上面的代码进行改写会产生什么样的效果呢?
function loadScript(src, callback) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
}
}
loadScript("/my/script.js")
.then(script => loadScript("/my/script2.js"))
.then(script => loadScript("/my/script3.js"))
.then(script => {
// ...加载完所有脚本
})
.catch(alert);
在上面的代码中,我们利用Promise,优化了原本的代码,使得代码更加清爽易读。让我们再来深入想想,Promise是如何做到对异步回调的优化的。
- 合并错误。在Promise中,我们让Promise对象产生的错误拥有了“冒泡“性质,使得产生的错误会沿着Promise链向后传递,直到被catch捕获为止。每个Promise链产生的错误都会向后传递,被catch统一处理,不再需要单独进行异常处理。
- 消除嵌套。Promise通过以下两部分,对嵌套进行消除。
- 延迟绑定。先创建Promise对象,再利用then来设置回调函数,为了避免回调任务对主线程的占用,使用微任务进行优化。
- 返回值穿透。利用then可以直接获取到回调函数的返回值,而无需对回调函数进行包裹。