理解Promise

168 阅读1分钟

页面中的任务都运行在渲染进程的主线程上。当我们需要进行一项比较耗时的任务时(例如,从网络下载资源文件时),为了避免主线程被长时间占用产生卡顿,我们会采用异步回调的方式。将耗时任务放到其它的进程中执行,当进程处理完主线程分配的耗时任务后,该任务会被添加到消息队列中,供主线程触发回调。

凡事皆有代价,异步回调的影响体现在代码的编写上。例如,当需要依次加载若干脚本时:

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) { 
            // ...加载完所有脚本
        }); 
        // ...错误处理
    });
    // ...错误处理
});

在这个例子中,我们仅使用了简化的代码作为示例,就已经显得有些混乱了。而在实际开发中回调的复杂性要远高于这个示例。为了解决这个问题,先来看看代码混乱的原因是什么:

  1. 回调函数的嵌套调用。每个异步任务都需要先得到上一个异步任务处理的结果,而上一个异步任务的回调函数内部又会有新的独立内容。按此层层嵌套,代码杂糅在一起,可读性很差。
  2. 回调任务可能失败。回调任务并不能保证一定成功,因此在每一层回调中还需要处理上一个异步任务失败的情况,进一步增加了代码的复杂度。 因此,我们需要一种能够消除嵌套调用,且能够合并回调任务失败产生的错误的方法。使用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可以直接获取到回调函数的返回值,而无需对回调函数进行包裹。