异步进阶——Promise

268 阅读5分钟

写在前面

在上一篇中,介绍了回调函数实现异步操作,其中存在两个主要的问题,第一是缺乏顺序性,第二是缺乏信任性。随着 ES6 发布后 Promise 对异步操作的处理进行了升级,大多数异步操作都将回调函数转移到了 Promise 。那么 Promise 对比回调函数,做了哪些优化呢?

答案是,Promise 解决了回调函数的缺乏信任性的问题。

那 Promise 是怎么解决回调函数的缺乏信任性的问题的呢?上篇讲到,缺乏信任其实是因为控制反转的问题,将一段代码的真正执行权交给了别人,导致的不信任问题。那如果再将控制权再反转回来呢?也就等同于不把控制权交出去,自己的事还是得自己做。但是希望异步的第三方给我们提供任务何时结束的能力,然后由我们自己决定接下来怎么做。这就是 Promise 范式的核心思想。

1. Promise 并没有摒弃回调函数

Promise 的实现并没有摒弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任的中介机制。

假设工具 ajax(url, 成功回调, 失败回调)存在。

回调函数实现的异步

function success() {
    console.log('我是回调,我成功了')
}
function fail(){
    console.log('我是回调,我失败了')
}
ajax('http://some.url.1', success, fail)

Promise 实现的异步

function success() {
    console.log('我是回调,我成功了')
}
function fail(){
    console.log('我是回调,我失败了')
}
var p = new Promise((resolve, reject){
    ajax('http://some.url.1', resolve, reject)
})
p.then(success, fail)

由上述两种实现方式来看,在进行异步操作时,还是得给异步操作传递回调函数让其调用,传递了两个,一个是 resolve, 一个是 reject。异步操作完成后如果成功调用 resolve,并将值作为 resolve 的参数传递过来,如果失败调用 reject,将失败理由作为 reject 的参数传递过来。

异步操作在 Promise 的包裹下执行,运行结果被 Promise 实例保存起来了,通过 then 方法传递回调获取运行结果。

Promise 相当于是一个中介,提供给异步操作的两个回调并不是实际要做的事,而是传递消息用的回调。当异步操作结束后通过回调传递过来消息,由 Promise 内部来调用真正要执行的函数,在真正的回调函数调用的过程中可能存在的很多问题都能由 Promise 这个中介解决和避免。

因此 Promise 实现的异步并没有脱离回调函数,都需要异步操作在操作完成后调用这边提供的回调函数。

这样一来,你自己想做某事的想法并没有告诉过别人,而是跟别人说,等异步操作完成了跟我说一声,你这边接收到异步操作完成的消息后,由你去实现想法,因此这个想法可以是你在开始调用异步操作前想好的,也可以是在拿到异步操作的结果后想好的。但对于回调函数实现的异步操作来说,你这个想法必须提前想好,必须在调用异步操作的时候同时传递过去。

回调函数实现的异步操作必须在初始化异步操作时定义回调,才能拿到异步操作的结果

Promise 实现的异步操作因为 Promise 实例保存了异步操作的结果,因此可以通过 Promise 实例随时拿到异步操作的结果,通过 then 随时定义回调

2. Promise 相比于回调函数的优点

Promise 相比于回调函数最大的优化就是解决了缺乏信任性的问题。Promise 的意思是承诺,期约,是一个可靠的中介机制。但仍没解决缺乏顺序性的问题,Promise 和 回调函数一样,都不符合大脑的顺序性直觉。

Promise 针对回调函数缺乏信任性的问题一一解决了,如下所示。

2.1. Promise 会自动防止 Zalgo 出现(调用过早)**

Zalgo 是啥,在 JS 里指的是一个任务有时同步完成,有时异步完成,导致不同的效果。如下所示:

function fn() {
    console.log('B')
}
function zalgo(num){
    if(num % 2 === 0){
        fn()
    } else {
        setTimeout(() => {
            fn()
        }, 0)
    } 
}
zalgo(2) // 同步操作 输出为 B A
//zalgo(1) // 异步操作 输出为 A B
console.log('A')

以上函数的调用因为传递参数的不同使得 fn 回调函数可能被同步调用,也可能被异步调用,这就很难办。

但是如果是 Promise 的话就不会出现这种情况,如下:

function fn() {
    console.log('B')
}
function zalgo(num){
    return new Promise((resolve, reject) => {
        if(num % 2 === 0){
            resolve()
        } else {
            setTimeout(() => {
                resolve()
            }, 0)
        } 
    })
}
zalgo(2).then(fn) // 异步操作 输出为 A B
//zalgo(1).then(fn) // 异步操作 输出为 A B
console.log('A')

Promise 实现的异步操作会将函数里无论是异步还是同步,都会转变成异步操作的。所以就避免了 zalgo 问题。

2.2 避免回调函数被调用多次

传统的回调函数实现的异步操作可能会存在这样的情况:

function fn(){
    console.log('A')
}
setTimeout(()=>{
    fn()
    fn()
}, 1000)
// 1s 后输出 A A

Promise 的实现中规避了这种情况,因此,用 Promise 的方式改造上述异步操作如下:

function fn(){
    console.log('A')
}
var p = new Promise((resolve, reject) => {
    setTimeout(()=>{
        resolve()
        resolve()
    }, 1000)
})
p.then(fn)
// 1s 后输出 A

3. Promise 存在的问题

3.1 Promise 无法取消

3.2 Promise 不支持进度通知

Promise 在 pending 状态时不知道是尚未开始,还是正在执行中。