用了几年的Promise,竟然还搞不清楚返回值是什么~

7,986 阅读25分钟

一、前言

大家好,我是疯狂的小波,一个热爱分享实战经验的前端开发。

这几天在开发功能时,调用同事写的函数,发现Promise在部分场景下没有手动变更状态。大概长下面这个样子:

function test(flag) {
    return new Promise((resolve, reject) => {
      // do something
      // ...
      if (flag === 1) {
          resolve()
      } else if (flag === 2) {
          reject()
      }
    });
}

于是就产生了下面这样的对话:

  • 我:“调用 test 的时候, 如果这个flag值是3,没有手动设置状态,那不是也会返回一个值为undefined的完成状态的promise?”
  • 同事:“不是啊,如果是3promise 不会往下执行。”
  • 我:“啊?我怎么记得没有手动处理也是默认返回完成状态的promise,只是值为undefined。”
  • 同事:“???”

然后自己再仔细想想,是啊,没有手动处理不可能默认返回完成状态。Promise 内部状态的变更,大部分都是在异步操作中处理,这样的话,如果没有执行到变更状态的代码时,就已经是完成状态了?显然是不可能的。

正确的应该是没有执行到变更状态代码时,构造函数返回的 Promise 始终是待定状态。

这应该也是大部分人的正确认知。那我为什么会产生这种基础的错误认知?

在此之前我还特意看过 MDNPromise 所有相关的文档,当时自认为对 Promise 已经足够了解了。没想到没过多长时间,一个这么基础的问题瞬间打回原形。

后来仔细想了想,让我产生这种误解的原因,还是理解的不够深刻,当时只是通过死记硬背的方式把文档内容记忆了下来,导致时间长了之后,记忆开始有点混乱了。

“没有手动返回,就返回完成状态的promise,只是值为undefined”,这个是 thencatch 方法,以及 async 函数返回值的其中一个规则,而不是构造函数的。

二、对 Promise 真的足够了解吗?

出了上面这个事情之后,再仔细想想,好像平常在用 Promise 进行开发的时候,有时也会遇到模棱两可或者拿不准的地方;再看看现在项目中的代码,发现也有很多不简洁、或者是使用错误的地方。

比如项目中的这种错误代码 ❌:

const wifiInfo = await getWifiInfo().catch();

还有这种不简洁的代码 😭:

async function getData() {
  return await fly.post('/home/getData');
}

所以就把平常使用过程中存疑的、还有这种使用错误的地方,简单总结了下面几条:

  1. Promise 在处理多个异步操作时,通常进行链式调用,如:p1.then().then().catch()。那 Promise 在链式调用时,是通过什么方式进行流转的?为什么可以通过链式的写法进行调用?
  2. 为什么.catch可以捕获前面所有的异常?
  3. .catch().catch(() => {}) 对异常处理区别是什么?
  4. 在使用 async/await 时,async 函数的返回值是什么?为什么async 函数具有传染性。
  5. return await直接 return 是否一样?
  6. 还有在涉及到多个Promiseasync 方法嵌套调用时,可以准确判断函数的返回值吗?如果调用时其中有.catch并被捕获,最终的返回值又是什么?

为了解决这些困扰,我把文档又全部重新理了一遍,并且结合demoPromise的源码,总算把这些问题都搞清楚了。下面把我的解决过程和结论分享出来,如果在使用 Promise 的时候你也有过类似的困扰或其他的问题,相信一定会有所收获的。让我们以后使用 Promiseasync/await 时都能够快速、精准的做出判断,再也不被这些问题困扰了 💪💪💪。

三、简单的基础回顾

在此之前,需要先简单回顾下 Promise2个基本内容:Promise状态 以及 Promise() 构造函数

我们后续的内容,基本都会基于这2个来展开。

Promise状态

除了我们自己通过 Promise() 构造函数创建的 Promise对象 ,通常我们使用一些第三方 API,也会返回一个 Promise对象 (如 axios)。

Promise对象 有3种状态。

  • 待定(pending):初始状态,既没有完成,也没有失败
  • 完成(fulfilled):操作成功完成
  • 失败(rejected):操作失败

当由 pending 状态变更为 fulfilledrejected 状态后,不会再变更。

Promise() 构造函数

包装不支持 Promise(返回值不是 Promise)的函数,返回一个 Promise对象。常用于处理异步操作可能产生的不同结果。

new Promise((resolve, reject) => {
  // do something
  // ...
  // 模拟异步操作,如接口请求
  setTimeout(() => {
      if (flag) {
          resolve("完成传递的值")  // fulfilled
      } else {
          reject("失败传递的值")  // rejected
      }
  }, 0)
});

构造函数执行时,会返回一个 pending 状态的 Promise对象,构造函数包裹的函数会立即执行。在异步执行到 resolve() 时,Promise对象 的状态会变更为 fulfilled 状态;执行 reject() 时,状态会变更为 rejected 状态

注意:如果没有执行 resolvereject 函数,则返回的 Promise对象 始终为 pending 状态,后续的 thencatchfinally 回调方法也不会执行。

四、链式调用的根本原因:实例方法的返回值

then()catch()finally() 实例方法始终都会返回一个新的 Promise对象。这也是为什么 Promise 能链式调用的根本原因。每次调用实例方法时,都会返回一个 Promise,这个返回值就又可以继续调用实例方法了。

但是不同的实例方法、不同场景下,返回的 Promise 会有区别,这就导致在后续链式调用时,代码执行逻辑也会不一样。所以弄清楚实例方法的返回值至关重要。

这里 then()catch() 的返回值原则是一致的;finally() 有点区别。

.then().catch()

.then(onFulfilled[, onRejected]);

.catch(onRejected);

then 方法接受2个参数:onFulfilledPromise对象 变更为 fulfilled 状态的回调;onRejectedrejected 状态的回调(可选)。这2个函数都有一个参数,接受状态变更时传递的值。

.catch(onRejected) 等同于 .then(undefined, onRejected),只是它的一个语法糖,所以在返回值规则上,他们也是相同的。

返回值原则

1、如果 thencatch 中的回调函数(onFulfilledonRejected):
  • 1.1、返回一个值 A,则实例方法返回 状态为 fulfilled、值为 APromise
  • 1.2、没有返回值,则实例方法返回 状态为 fulfilled、值为 undefinedPromise
  • 1.3、抛出一个错误,则实例方法返回 状态为 rejected、值为抛出的错误Promise
  • 1.4、返回一个 Promise(P),则实例方法返回 状态、值与 P 相同的 PromiseP 状态变更时,这个 Promise 也会变更。

示例如下:

const p = Promise.resolve("f1");

p.then(res1 => {
    console.log(res1) // f1
    return 'f2'
}) // 返回 fulfilled、值为f2 的Promise。继续执行下一个 then
.then(res2 => {
    console.log(res2) // f2
}) // 返回 fulfilled、值为undefined 的Promise。继续执行下一个 then
.then(res3 => {
    console.log(res3) // undefined
    throw 'r1';
}) // 返回 rejected、值为r1 的Promise。继续执行下一个 catch
.catch(res4 => {
    console.log(res4) // r1
    return Promise.resolve("f3");
}) // 返回 fulfilled、值为f3 的Promise。继续执行下一个 then
.then(res5 => {
    console.log(res5) // f3
}) // 最终返回 状态为fulfilled、值为undefined 的 Promise

async 函数的返回值原则,与上面的这些原则是一样的。所以弄清楚这些对我们用好 async/await 也有很大的帮助。

2、如果 thencatch 中没有对应状态的回调函数(或参数不是函数类型),那就会返回一个 与调用该方法的 Promise 相同的 新Promise对象

从表现上来看就是直接跳过没有对应状态回调的实例方法。比如调用 Promise.reject().then(onFulfilled) 时,由于 .then 中没有 rejected 状态的回调,.then() 方法会直接返回与 Promise.reject() 相同的 Promise

示例如下:

const p = Promise.resolve("f1");

p.then(res1 => {
    // do 1
    return Promise.reject("r1");
}) // 返回 rejected、值为r1 的Promise(P1)
.then(res2 => {
    // do 2
}) // 由于这个then中没有rejected状态回调,这里do 2不会执行,then直接返回 与P1相同的 rejected、值为r1 的Promise
.catch(res3 => {
    // 所以前面2个任意一个then中回调函数执行错误或返回rejected状态,都会执行这里
})

上面代码中,当执行 do 1 时返回一个 rejected 状态的 Promise(P1);执行到第二个 .then 时,由于没有 rejected 状态的回调,这个 .then() 会直接返回与 P1 相同的 Promise,继续执行到 .catch,则会进入 .catch 的回调。从表现上看,中间会直接‘跳过’ do 2

这也是为什么:我们在链式调用时只在最后写 .catch(),之前的错误都能够捕获到;

同理,当我们希望某个操作不管是完成还是失败,都可以继续走后续的逻辑时,就可以将 .catch 写在指定的 .then 方法后,这样不管这个 .then 是否触发异常,也不会影响后续的链式调用。

const p = Promise.resolve("f1");

p.then(res1 => {
    return doSomething();
})
.catch(res2 => {
    // doSomething() 返回 rejected 时执行
    return null;
})
.then(res3 => {
    // 无论 doSomething() 返回什么,都会执行这里
    // doSomething() 返回 fulfilled 时:上面的 .catch() 没有对应状态回调,也会直接返回,res3 值为 doSomething返回值
    // doSomething() 返回 rejected 时:上面的 .catch() 捕获到异常,返回null,基于上面提到的返回值规则,此时 res3 值为 null
})

通常情况下,Promise 中的这种链式调用使用 async/await 会有更好的体验。

Promise 源码实现看 then 方法的返回值

为了进一步加深理解,我们从代码层面来看看,到底 Promise 内部是怎么处理的。

模拟 Promise 源码如下。为了方便理解,基本上每一步都加了注释,我们这里主要关注的是 then 方法的处理,在注释中也标识了上面返回值规则对应的代码处理:

class myPromise {
    constructor(func) {
        // 构造函数初始化时,返回的Promsie对象状态默认为 'pending' 状态
        this.status = 'pending';
        // 值默认为undefined
        this.result = undefined;
        // 完成状态回调函数数组
        // 因为同一个Promise对象可以多次调用.then方法,当在pending 状态调用.then方法时,先在该数组中存储.then的完成状态回调函数
        // 等到Promise状态变更为fulfilled时,再循环执行这些回调函数
        this.onFulfilledCallbacks = [];
        // 失败状态回调函数数组
        // 原理和onFulfilledCallbacks类似
        this.onRejectedCallbacks = [];
        // 构造函数包装的函数,初始化时立即执行。
        // 这里.bind(this),是为了指定调用resolve、reject方法时函数内this为当前实例,否则调用resolve、reject时获取不到函数内this的指向
        func(this.resolve.bind(this), this.reject.bind(this));
    }
    
    // 将状态变更为fulfilled并赋值,如果在此之前绑定了完成回调则依次执行
    resolve(result) {
        if (this.status === 'pending') {
            this.status = 'fulfilled';
            this.result = result;
            this.onFulfilledCallbacks.forEach(callback => {
                callback(result)
            })
        }
    }
    
    // 将状态变更为rejected并赋值,如果在此之前绑定了失败回调则依次执行
    reject(result) {
        if (this.status === 'pending') {
            this.status = 'rejected';
            this.result = result;
            this.onRejectedCallbacks.forEach(callback => {
                callback(result)
            })
        }
    }
    
    // 今天的主角,then实例方法
    then(onFulfilled, onRejected) {
        // 创建一个新的Promise对象,并最终返回
        const returnPromise = new myPromise((resolve, reject) => {
            // 如果调用.then方法时,已经是'fulfilled'状态,则直接异步执行回调
            if (this.status === 'fulfilled') {
                // 方法A:根据完成状态回调,设置 returnPromise 的状态及值
                setTimeout(() => {
                    if (typeof onFulfilled === 'function') {
                        try {
                            let callBackResult = onFulfilled(this.result);
                            // 规则1.1、1.2、1.4 的集中处理,根据回调函数返回值决定 returnPromise 的状态及值
                            resolvePromise(returnPromise, callBackResult, resolve, reject);
                        } catch (e) {
                            // 规则1.3、onFulfilled回调抛出错误,返回rejected、值为抛出错误的 Promise
                            reject(e);
                        }
                    } else {
                        // 规则2、没有对应状态的回调函数,返回与调用者相同状态(因为这里是fulfilled状态,所以直接resolve)、值相同的 Promise
                        resolve(this.result);
                    }
                });
            } else if (this.status === 'rejected') {
                // 方法B:根据失败状态回调,设置 returnPromise 的状态及值
                setTimeout(() => {
                    if (typeof onRejected === 'function') {
                        try {
                            let callBackResult = onRejected(this.result);
                            resolvePromise(returnPromise, callBackResult, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    } else {
                        // 规则2、没有对应状态的回调函数,返回与调用者相同状态(因为这里是rejected状态,所以直接reject)、值相同的 Promise
                        reject(this.result);
                    }
                });
            } else if (this.status === 'pending') {
                this.onFulfilledCallbacks.push(() => {
                    // 同上面方法A
                    // ...
                });
                this.onRejectedCallbacks.push(() => {
                    // 同上面方法B
                    // ...
                });
            }
        })
        
        // .then 实例方法始终返回1个新的Promise对象
        return returnPromise
    }
    
    // 同.then方法,只是 .then(undefined, fn) 的语法糖,内部直接调用即可
    catch(fn){
        return this.then(undefined, fn);
    }
}

/**
 * 根据.then中回调函数返回值,决定.then实例方法返回的Promise的状态及值
 * @param  {promise} returnPromise  .then方法返回的新promise对象
 * @param  {any} callBackResult     .then中onFulfilled或onRejected回调函数的返回值
 * @param  {function} resolve       returnPromise的resolve方法
 * @param  {function} reject        returnPromise的reject方法
 */
function resolvePromise(returnPromise, callBackResult, resolve, reject) {
    // 返回值不能是returnPromise本身,否则会循环引用
    if (callBackResult === returnPromise) {
        throw new TypeError('Chaining cycle detected for promise');
    }
    
    // 规则1.4、回调函数返回一个Promise,则.then方法返回的returnPromise与之状态、值同步
    if (callBackResult instanceof myPromise) {
        // 在Promises/A+规范中,如果callBackResult完成时返回的仍旧是一个promise(B),则returnPromise会与B的状态、值同步。所以这里用到了递归方法处理完成的返回值。
        callBackResult.then(newResult => {
            resolvePromise(returnPromise, newResult, resolve, reject)
        }, reject);
    }
    // 规则1.1、1.2、回调函数返回一个值A、或没有返回值,.then方法返回fulfilled状态,值为A或undefined的Promise。
    // 所以这里直接是以callBackResult作为returnPromise的完成值
    else {
        return resolve(callBackResult);
    }
    // Promise 中为了让链式调用的实现更具有通用性,规定:只要回调函数返回值暴露出一个遵循 Promises/A+ 协议的 then 方法,也会被当作Promise来处理结果。
    // 所以在实现中,下面这个场景的判断,如果是一个含有.then方法的object或function,则会执行.then方法进行求值,与上面返回值是Promise时思路是一致的。
    // 但是这种情况在日常开发中比较少见,这里为了方便理解,我把这段代码注释了,大家在这里只需要知道有这个机制就行。
    // else if (callBackResult !== null && ((typeof callBackResult === 'object' || (typeof callBackResult === 'function')))) {
    //    try {
    //        var then = callBackResult.then;
    //    } catch (e) {
    //        return reject(e);
    //    }

    //    if (typeof then === 'function') {
    //        let called = false;
    //        try {
    //            then.call(
    //                callBackResult,
    //                y => {
    //                    if (called) return;
    //                    called = true;
    //                    resolvePromise(returnPromise, y, resolve, reject);
    //                },
    //                r => {
    //                    if (called) return;
    //                    called = true;
    //                    reject(r);
    //                }
    //            )
    //        } catch (e) {
    //            if (called) return;
    //            called = true;

    //            reject(e);
    //        }
    //    } else {
    //        resolve(callBackResult);
    //    }
    //}
}

了解了源码之后,再来看之前的代码,是不是会觉得更加清晰了:从 Promise 实例初始化,到状态变更,再到调用 .then 方法的内部处理,以及不同状态下实例方法的返回值,最终根据这个返回值是怎么完成的链式调用。

可以再结合源码来看看下面这段代码:

const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("疯狂的")
    }, 1000)
});

p.then(res1 => {
    return Promise.resolve(res1 + "小波");
})
.then(res2 => {
    throw ('没给 ' + res2 + ' 点赞');
})
.catch(res3 => {
    console.log(res3)
})

怎么样,如果结合源码的执行来看,是不是整体的感觉比之前更清晰了。我自己是感觉对 Promise 的掌握又更上一层楼了,至少不会在短时间内再忘记了~。你也点赞收藏下呗,免得时间长了之后忘记又找不到啦😊~

啥?为啥会抛出异常?看到这里了都不点个赞,不得出异常吗~

.finally()

.then.catch 的使用在日常生活中非常多,所以分享的内容会多一点。下面我们再来看看另外一个实例方法 .finally()

.finally(onFinally);

Promise 结束后,无论结果是 fulfilledrejected,都会执行 onFinally 回调函数。与 .then.catch 不同的是,onFinally 回调函数没有参数。返回值也有所不同:

返回值原则

返回与调用该方法的 Promise对象 相同的 新Promise对象

// finally:返回 值为2、状态为rejected的Promise
Promise.reject(2).finally(() => {})

// then:返回 值为undefined、状态为fulfilled的Promise
Promise.reject(2).then(() => {}, () => {})

与上面的 thencatch 中第2条规则:没有对应状态的回调函数时,返回原则是一样的。有点区别的是:如果在 onFinally 回调中,抛出异常或返回 rejectedPromise,则 .finally() 会返回这个 rejectedPromise,不过通常情况下并不会这样使用。

小结

Promise 基于 then()catch()finally() 实例方法返回一个 Promise对象,达到了可以链式调用的效果。只要我们掌握这些不同场景下不同的返回值,对于 Promise 的链式调用,就可以很清楚的知道执行逻辑了。有些其他的框架也有这种设计思路,比如 Jquery 的实例方法,就是返回的 Jquery实例对象,也是可以很方便的进行链式调用。

现在再看看刚开始总结的第12个疑问:

  1. Promise 在处理多个异步操作时,通常进行链式调用,如:p1.then().then().catch()。那 Promise 在链式调用时,是通过什么方式进行流转的?为什么可以通过链式的写法进行调用?
  2. 为什么.catch可以捕获前面所有的异常?

是不是现在感觉非常清晰了~,如果还有疑问,可以在评论区与我交流哦~。

顺便也可以想想第3个问题:3..catch().catch(() => {}) 对异常处理区别是什么?如果对上面的返回值规则理解了的话,应该就能发现区别了。

没有发现也没有关系,下面介绍异常处理时,我们再来揭晓这个答案。

五、链式调用的更优选择 async/await

当链式调用的链路比较长时,一般会使用 asyncawait 关键字来实现,代码会更清晰和简洁。

类似下面这样:

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`这里是疯狂的小波,听到请回答: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

async/await 中使用 try/catch 进行异常捕获,与 then/catch 类似。可以像上例中,统一进行捕获,这种中间任一个函数抛出异常或 rejected,就会跳转到 catch 中执行。与 then/catch 中,最后一个 .catch 可以捕获前面所有异常效果是一样的。

使用 async 时,其中一个容易出错的也是函数的返回值。特别是嵌套调用等复杂场景,如果不能明确每一步的返回值,就容易产生很多问题。

async 函数返回值规则

始终返回一个新的 Promise对象

1、then()catch() 的返回值规则相同。如果 async 函数:

  • 返回一个值 A,则返回 状态为 fulfilled、值为 APromise
  • 没有返回值,则返回 状态为 fulfilled、值为 undefinedPromise
  • 抛出一个错误,则返回 状态为 rejected、值为抛出的错误Promise
  • 返回一个 Promise(P),则返回 状态、值与 P 相同的 PromiseP 状态变更时,这个 Promise 也会变更。

示例如下:

async function foo() {
    return 'f1' // == return Primise.resolve('f1')
} // 返回 fulfilled、值为f1 的Promise

async function foo() {
    // 没有 return
} // 返回 fulfilled、值为undefined 的Promise

async function foo() {
    return pormiseFunction()
} // 返回与pormiseFunction()返回值相同的Promise

2、await 等待的函数 抛出异常或返回 rejectedPromise 时。

没有异常捕获:则后续代码不会执行, async 函数直接返回 rejectedPromise,值为抛出的异常值或 rejected 值。

async function foo() {
    const result = await doSomething(); // doSomething 返回 rejected;
    // 下面代码都不会执行
    const newResult = await doSomethingElse(result);
    return newResult;
}

foo() // 返回和 doSomething() 相同的 rejected 的 Promise

内部有异常捕获时:异常时,控制器执行 catch 块代码,再根据上面的第1条规则返回

async function foo() {
  try {
    const result = await doSomething(); // doSomething 返回 rejected;控制器直接转到catch
    // 下面代码都不会执行
    const newResult = await doSomethingElse(result);
    return newResult;
  } catch(error) {
    // 捕获异常。
    return null;
  }
}

foo() // 返回一个 fulfilled 的 Promise,值为 null

整体上来说,和 then()catch() 的返回值规则是差不多的。

async 函数返回值获取

有时 async 函数会直接返回一个值,如果习惯性的直接使用这个返回值,就会出错。

由于 async 函数的返回值是 Promise, 所以获取返回值时也需要通过 Promise 的方式来获取。

async function foo() {
    return 'f1'
}

console.log(foo() === 'f1')  // ❌  async 函数返回的是一个promise对象

foo().then(res => console.log(res)) // ✅ f1。 

或者再次使用 async/await 获取,这也是为什么通常说 async 函数具有传染性的原因。

async function getfoo() {
    const result = await foo();
    console.log(result);
}

return await 和单独 return 的区别

另外一个容易产生误区的地方就是 return await 和单独 return,经常在使用过程中发现2者都有使用,但是感觉效果又是一样的。下面我们就来看看他们到底有什么区别。

1、当有 try/catch 进行处理并且 await 的函数抛出异常时,这2者有一些细微的差异。

如下,当 doSomething() 抛出异常或返回 rejected 时:

// 使用 return await
async function foo() {
  try {
    return await doSomething();
  } catch(error) {
    return null
  }
}
// foo() 会进入catch回调,最终返回状态为fulfilled、值为null的Promise
// 使用 return
async function foo() {
  try {
    return doSomething();
  } catch(error) {
    return null
  }
}
// foo() 会直接返回 rejected 的Promise,值为 doSomething() 抛出的异常或返回的 rejected 值

return await doSomething(),将等待 doSomething() 执行出结果 (fulfilledrejected),如果是 rejected,将会在返回前抛出异常。

而直接 return doSomething(),不管 doSomething() 返回的 Promisefulfilled 还是 rejected 都将会直接返回这个 Promise 本身

2、而没有 try/catch 时,使用是一样的。

async function foo() {
  // do something
  return await doSomething();
}

async function foo() {
  // do something
  return doSomething();
}
  • 如果 doSomething() 正常执行,返回 f1await 会进行求值,获取到 f1 的值,由于 async 函数会对返回值进行隐式替换,最终返回 Promise.resolve(值),上面最终2种写法 foo() 都会返回与 f1 状态、值同步的 Promise
  • 如果 doSomething() 返回异常 r1:同理,2种写法 foo() 都会返回 rejected、值为 r1值Promise

像这个例子中,如果中间没有其他的 await 关键字,使用 async/await 就很多余了,就像最开始的例子中一样。可以直接写成普通函数就行。 function foo() { // do something; return doSomething(); }

应该怎么记这种区别?

如果像上面举例这样记忆,就显得太复杂了,显然这不是一个好的方法。

其实我们只需要知道一点:await 关键字是获取 Promisefulfilled 状态的值,如果 Promiserejected 状态,则会直接抛出异常。

基于这个原则,再来看上面的例子,其他的代码都很熟悉,是不是能得到相同的结果。这个才是根本原因,上面的场景只是由这个原因导致的结果。

小结

我们现在再来想想上面提的第45个疑问:

  1. 使用 async/await 时,async 函数的返回值是什么?为什么async 函数具有传染性。
  2. return await直接 return 是否一样?

如果能够立马想到答案,那说明对上面的内容基本上掌握啦 😄~

async 小技巧:同步开始,异步处理

既然已经说了这么多 async 的内容,再分享一个最近发现的一般人不会注意的小技巧。

常规的处理多个异步操作时,如果有依赖上一步的数据,通常都是链式调用。就像我们上面的例子中一样,等待上一个调用完成后,再进行处理下一个。

而有些场景下,异步操作在调用时没有依赖,但是在处理上有顺序要求。比如商品详情页2个接口,获取商品详情信息、获取当前页面配置的促销信息,调用时没有数据依赖,但是只有当商品获取成功时才展示促销信息。此时接口调用就可以同步开始获取,进行异步处理,这样可以更快的让页面呈现出完整数据。如下:

  • 常规的异步处理 - 页面需要 3s 呈现出完整数据:
async function getPageData() {
  const detail = await getDetail(); // 获取商品详情,假设2秒后返回数据
  setDetail(detail)
  
  const promotion = await getPromotion(); // 等待上一个接口获取完成,再获取促销信息,假设1秒后返回数据
  setPromotion(promotion)
}
  • 同步开始,异步处理 - 页面 2s 呈现出完整数据:
async function getPageData() {
  const detail = getDetail(); // 获取商品详情,假设2秒后返回数据
  const promotion = getPromotion(); // 获取促销信息,假设1秒后返回数据
  
  setDetail(await detail); // 2秒后赋值
  setPromotion(await promotion); // 上面执行后立即执行
}

2种方式最终实现的效果相同,第1种方式页面完整呈现时间是2个接口请求时间相加,而第2种方式是最长的一个接口请求时间。在某些场景下还是比较有用的,需要的话快使用试试吧~。

六、常见错误 Uncaught (in promise)

还记得我们上面提到的这个疑问吧:.catch().catch(() => {}) 对异常处理区别是什么?

在日常开发中经常看到这种 Uncaught (in promise) 错误,不管是我们自己写得代码,或者是第三方的代码,可能都会抛出这样的异常。就是当 Promise 变更为 rejected 时,没有被 catch 方法捕获到。

比如下面这样:

doSomething().then(function(result) {
  // doSomethingElse
});

doSomething() 返回 rejected 状态时,由于没有 .catch 方法捕获异常,就会直接在控制台中抛出错误 Uncaught (in promise)

所以通常都要在链条最后加一个 .catch 来处理这种异常。

异常处理

有时为了避免抛出异常,或者不管 Promise 是完成还是失败时都继续向下执行,但是又没有其他的业务处理时,我们会简单的加一个空的 .catch()

❌ 错误处理:直接 .catch() ,不添加回调函数。

doSomething().then(function(result) {
  // doSomethingElse
}).catch();

这样写并不会捕获到异常。还记得上面的返回值规则吗? .then().catch() 的返回值是根据内部对应状态的回调函数决定的,单纯的 .catch() 没有回调函数,就会匹配其第2条原则:没有对应状态的回调函数,那就会返回一个 与调用该方法的 Promise对象 相同的 新 Promise 对象,所以这里当 doSomething() 返回 rejected 时,.catch() 后还是会返回 rejectedPromise。所以最终还是抛出了 Uncaught (in promise) 错误。

✅ 正确处理:.catch(() => {})

async function foo() {
    const result = await doSomething().catch(); // ❌ 直接.catch() 不会捕获异常。所以这里当rejected时,代码不会继续向下执行,foo() 直接返回 rejected 的 promise
    const result = await doSomething().catch(() => {}); // ✅ catch有回调,可以捕获,doSomething() rejected 时,result 值为 undefined,代码继续向下执行
    doSomethingElse(result)
}

全局异常捕获

那如果我们不希望每一个地方都去单独捕获,或者第三方内部的代码抛出这种错误,我们没有办法直接捕获的时候,应该怎么办?

可以通过 unhandledrejection 方法来全局捕获这种错误,然后在监听事件中做些自己的处理,比如需要做自定义的异常监控或问题收集时。

window.addEventListener("unhandledrejection", event => {
  /* event.promise: 异常的promise对象
     event.reason: 异常的 rejection 原因 */
  
  // do something
  
  event.preventDefault(); // 不将错误打印到控制台
}, false);

七、链式调用中的异步性

到了这里,我们开始时提出的几个疑问,基本都能找到答案了。

这一章想单独介绍下 链式调用中的异步性,也就是代码的执行时机(promise在事件循环机制中的简单表现),感兴趣的也可以看看。

除了链式调用时的返回值,如果不清楚回调函数的执行时机,有时也会遇到各种问题。

promise 中的回调函数执行遵循以下3条基本原则:

  • 当一个 Promise 状态变更为 fulfilled 或者 rejected 时,回调函数将被异步调用(由当前的线程循环来调度完成);
  • 即使是一个已经变成 rejected 状态的 Promise,传递给 then() 的函数也会被异步调用;
  • 链式调用 .then() 的回调函数,会按照插入顺序进行执行。

如下,我们有主函数执行语句,主函数中的定时器,以及 Promisethen 回调。根据上面的原则,then 回调是异步调用,setTimeout 也是异步调用,并且优先级低于 then 回调。最终的执行顺序如下:

setTimeout(() => {
    console.log('main setTimeout');
}, 0)

Promise.resolve(1).then(res => {
    console.log(res);
})

console.log('main script')

// 依次返回:
// main script
// 1
// main setTimeout

上面的代码中,只有单一的 then 处理,如果是链式调用,执行顺序是什么样的?

Promise.resolve(1).then(res => {
    console.log(res);
    return res + 1
}).then(res => {
    console.log(res);
    return Promise.resolve(res + 1)
}).then(res => {
    console.log(res);
    return new Promise((resolve, reject) => {
        console.log('start Promise')
        setTimeout(() => {
            console.log('then setTimeout');
            resolve('then Promise')
        })
    })
}).then(res => {
    console.log(res);
})

setTimeout(() => {
    console.log('main setTimeout');
})

console.log('main script')


// 上面的代码会依次返回:
// main script
// 1
// 2
// 3
// start Promise
// main setTimeout
// then setTimeout
// then Promise

在第一个示例的基础上,我们添加了多个 then 的链式调用。如果上一个 then 返回的是一个 fulfilledrejected 状态的 Promise,则后续 then 的回调会继续执行;如果返回的是 pending 状态的 Promise,则会等 Promise 的状态变更为 fulfilledrejected,再异步执行对应状态的回调。

还记得上面的 Promise 源码吗?如果结合源码再来看看这里的代码执行时机,这些文字描述,是不是变的更好理解了?

总结

扯了这么多,终于把开始提出的6个疑问搞清楚了~。不过你确定真的都掌握了吗?会不会过一段时间像我一样又忘了,所以点个赞收藏一下呗~,记住不迷路。如果还有疑问或者有其他的问题,也欢迎评论留言与我交流 🎉🎉🎉。

往期推荐