JavaScript - Promise

ES6 引入的一种异步编程方案,它会在未来的某个时间点交付异步操作的结果,这个结果既可以是成功的,也可以是失败的。

1. Promise 对象

  • 一个Promise对象代表一次异步操作,且在生成后被立即执行

  • Promise对象有三种状态:

    • pending : 初始状态,既不是成功,也不是失败状态

    • fulfilled : 意味着操作成功完成

    • rejected : 意味着操作失败

  • 通过new Promise()创建一个Promise对象

    • resolve将把 Promise 对象从pending变为fulfilled状态

    • reject将把 Promise 对象从pending变为rejected状态

    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1);
      }, 100);
    });

2. Promise 实例方法

  • then() 接受两个方法作为参数,分别用来指定成功状态与失败状态的回调,两个方法接受的参数由 Promise 对象中的resolvereject提供。

  • catch() 处理异常的方法,参数为一个方法,与then的第二个回调方法类似,可以用于处理失败状态。不同点在于catch也可以获取到回调方法中的代码异常,所以实际使用时,经常使用以下写法。

  • finally() 在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。该方法没有参数。

3. Promise 静态方法

  • Promise.resolve() 创建一个立即fulfilledPromise对象

  • Promise.reject() 创建一个立即rejectedPromise对象

  • Promise.all() 参数为一个Promise数组,用于统一处理一组Promise对象的异步调用结果

    当数组中所有的对象都为resolve时,新对象状态变为fulfilled

    当数组中有一个对象reject时,新对象变为rejected

  • Promise.race() 参数也为一个Promise对象数组,数组中有一个对象resolve时,新对象状态变为fulfilled;当有一个对象reject时,新对象状态变为rejectedrace方法常用于超时处理。

4. 链式调用

在 2 中,then()catch()finally() 方法以链状的形式被先后调用,其能够执行的原因在于这三个方法本身都将 return 一个Promise实例,返回实例能够继续调用Promise的实例方法。

5. Promise、setTimeout、async/await 执行顺序问题

a. js 事件循环机制(Event Loop)

  • 在了解这三者的执行问题前,先来回顾一下 js 事件循环机制。众所周知,js 自古以来就是一门单线程,非阻塞的语言。单线程意味着只有一个主线程来执行所有的任务,也就是说同一时间只能执行一个任务。

  • Event Loop:分为主线程、宏任务、微任务三部分

    • 主线程:可以直接执行的 js 代码

    • 宏任务:setTimeoutsetInterval

    • 微任务:Promise.then()new Promise主体部分的内容属于宏任务,可以直接执行。另外带 async 关键字的方法本身会返回一个 Promise 对象,可以理解为有 await 关键字的方法及其之前的部分为 Promise 对象的主体内容。带关键字await的方法之后的内容就是 Promise.then() 部分。

  • 执行机制关键点:

    • 优先执行主线程

    • 遇到宏任务时,将其放入宏队列,遇到微任务时,将其放入微队列

    • 主线程执行完毕后,执行微队列中的所有微任务直至清空

    • 执行一个宏任务,宏任务完成后,查看主线程或微队列是否为空,若为空则继续执行

    • 对于 setTimeout 而言,会在时间到了之后才会进入宏队列,而进入宏队列不意味着执行,也因此 setTimeout 内的代码的时机执行间隔是随机且略大于设定时间的。

  • 了解了机制之后,举一个例子

  • 挨个进行分析

    1. 首先主线程从 9 开始执行,毫无疑问先输出 script start;

    2. 10~22,23~25。两个 setTimeout 方法,宏任务,由于主线程还没执行结束,所以先放入宏队列,分别记为 M1 和 M2;

    3. 26 为带 async 关键字的方法,先进入,2~3 为主线程代码,所以输出 async1 start,然后执行 async2();

    4. async2()这个方法的 async 关键字在这个例子中其实是多余的,该方法可以视作一个普通的方法,所以为主线程内容,输出 async2;

    5. 4~5 为 await 之后,相当于 Promise.then 部分,主线程还没结束,扔进微队列,记为 m1;

    6. 主线程现在到了 27,28 为 Promise 对象主体,主线程内容,输出 promise1;

    7. 30~32,Promise.then 部分,扔进微队列,记为 m2;

    8. 33,主线程,输出 script end,到此主线程暂时执行完毕;

    9. 主线程完毕,开始看微队列,先进先出原则,首先执行 m1,输出 async1 end;

    10. 微队列没有结束,继续执行微队列中的 m2,故输出 promise2,到此微队列暂时也告一段落;

    11. 接下来开始执行宏队列中的 M1,11~12 相当于主线程代码块,直接运行,输出 promise3;

    12. 13 为 resolve 方法,执行后将 15~20 扔进微队列,记为 m3;

    13. 21 为当前宏任务主代码块内容,直接输出 setTimeout1。至此,M1 宏任务结束;

    14. 此时,微队列增加 m3,故要先去执行微队列,发现 16~18 又是一个宏任务,记为 M3;

    15. 直接执行 19,输出 promise4;

    16. 微队列再次清空,执行宏队列中的 M2,输出 setTimeout2;

    17. 一个宏任务结束,查看主线程与微队列都为空,继续执行宏队列的 M3,输出 setTimeout3,至此,代码全部执行完成。

  • 执行结果

  • 结语: 代码执行顺序问题引发的 bug 在平时工作中经常出现,了解 js 代码以及各个异步编程方法的执行机制,可以有效规避这类问题。

最后更新于

这有帮助吗?