JS -- (20) Promise

110 阅读5分钟

一. 之前异步串联产生的问题

Promise 是实现异步编程的一种解决方案。以往的异步操作,只能通过调用回调函数(把一个函数作为参数传到另一个函数中)来进行某些操作,来证明该异步操作已经完成!!!

但是由于串联多个异步操作是一个常见的问题,所以还用回调函数来表示完成状态,就会产生深度嵌套的回调函数(回调地狱)!!

具体问题如下:

<1> 回调嵌套
doA( function(){
    doB();

    doC( function(){
        doD();
    } )

    doE();
} );

doF();

在项目中,我们往往需要函数嵌套函数执行,可能比上述代码更为难以理解,所以就需要我们花费很多精力去思考它们的执行顺序。what's worse?实际上,我们还会在代码中加入各种各样的逻辑判断,就比如在上面这个例子中,doD() 必须在 doC() 完成后才能完成,万一 doC() 执行失败了呢?我们是要重试 doC() 吗?还是直接转到其他错误处理函数中?当我们将这些判断都加入到这个流程中,很快代码就会变得非常复杂,以至于无法维护和更新。

<2> 回调反转
// 回调函数是否被执行取决于 buy 模块
import {buy} from './buy.js';

buy(itemData, function(res) {
    console.log(res)
});

当我们使用回调的时候,这个回调函数是否能接着执行,其实取决于使用回调的那个 API是如何封装的,可能会出现以下情况:

  1. 回调函数执行多次
  2. 回调函数没有执行
  3. 回调函数有时同步执行有时异步执行

而对于这些情况,我们都需要相应的处理措施!!

<3> 回调地域
  • 代码难以复用

回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。

  • 堆栈信息被断开

同步执行:如果在A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。所以说,如果出错了,执行上下文栈是还保存的

异步执行:,比如执行 fs.readdir 的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。

【Promise没有解决该问题】

二. Promise

(1) 创建 Promise 对象

Promise对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个执行器函数作为参数,返回一个Promise实例。

一个 Promise 实例有三种状态,分别是pending、resolved和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由pending 转变resolved或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。

Promise构造函数的参数 ---- 执行器函数 有两个函数参数resolve() 、reject() ,调用两者,可以分别实现不同的状态改变!!!

注意:

Promise构造函数本身是同步的立即执行的函数,所以构造函数内部的代码是立即执行的,但在内部遇到 resolve()、reject() 函数时 ,才会进行异步操作,因为执行完resolve()、reject() 函数后,就会立马改变Promise的状态,就会调用.then() 方法中注册的处理函数,而这个回调函数属于微任务,会在本轮事件循环的末尾执行。

(2) 期约方法
  • Promise.resolve()

实例化一个解决的期约,且该期约的值就是传入的第一个参数

  • Promise.reject()

实例化一个拒绝的期约,且期约的拒绝理由就是传入的第一个

  • Promise.prototype.then()

该方法接收两个参数 onresolved() 、onRejected() 两个处理程序,如果提供的话,会在期约进入解决or拒绝状态时执行,因为Promise状态只能改变一次,所以两个处理程序是互斥的!!!

  • Promise.prototype.catch()

.catch() 方法用于给期约添加期约拒绝处理程序 onRejected(),相当于一个语法糖,调用它就相当于调用 Promise.prototype.then(null , onRejected())

  • Promise.prototype.finally()

该方法用于给期约添加 onFinally() 处理程序 ,该程序会在期约的状态发生改变后执行(不管是哪种改变都会执行),常常用于添加清理代码!!!

  • Promise.all()

     -   接收一个可迭代对象作为参数,可迭代对象中的元素会依次通过 Promise.resolve()转换为期约
     -   有待定 ----> 待定
     -   全解决 ----> 解决
     -   有拒绝 ----> 第一个拒绝
    
  • Promise.race() - 接收一个可迭代对象作为参数

     -   合成为最先解决or拒绝的期约
    

详见我博客

三. Promise 的局限性

  • 错误被吃掉
let promise = new Promise(() => {
    throw new Error('error')
});
console.log(2333333);

会正常的打印 233333说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。

  • 无法取消

Promise 一旦新建它就会立即执行,无法中途取消。

  • 无法得知 pending 状态

当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

源码实现