一、概述
1. Promise解决了回调处理异步问题时存在的两个弊端:回调地狱、难以处理错误。
2. Promise实例有三个状态:pending、fulfilled、rejected。
Promise实例有两个过程:pending->fulfilled、pending->rejected
3. Promise的局限性
(1)状态从pending变为fulfilled、或从pending变为rejected,一旦状态改变就不会再变。当promise实例被创建时,内部的代码就会立即被执行,而且无法从外部停止。比如无法取消超时或消耗性能的异步调用,容易导致资源的浪费。
(2)如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
(3)Promise处理问题都是一次性的,因为一个Promise实例只能resolve或reject一次,所以面对某些需要持续响应的场景时就会变得力不从心。比如上传文件获取进度时,默认采用的就是事件监听的方式实现。
下面来看一个例子:
1 | const https = require('https'); |
可以看到,Promise会接收一个执行器,在这个执行器里,需要把目标异步任务给放进去。在Promise实例创建后,执行器里的逻辑会立即执行,在执行过程中,根据异步返回的结果,决定如何使用resolve或reject来改变Promise实例的状态。
当用resolve切换到了成功态后,Promise的逻辑就会走到then中传入的方法里去,用reject切换到失败态后,Promise的逻辑就会走到catch传入的方法中。
这样的逻辑,本质上和回调函数中的成功回调和失败回调没有差异。但这种写法大大地提高了代码的质量。当我们进行大量的异步链式调用时,回调地狱不复存在了。取而代之的是层级简单、赏心悦目的Promise调用链。
二、Promise的创建
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。
1 | const promise = new Promise((resolve, reject) => { |
一般情况下,我们会用new Promise()来创建Promise对象。除此之外,还可以使用Promise.resolve和Promise.reject这两个方法来创建:
1. Promise.resolve
Promise.resolve(value)的返回值是一个promise对象,我们可以对返回值进行.then调用,如下代码:
1 | Promise.resolve(11).then(function(value){ |
resolve(11)会让promise对象进入确定(resolve状态),并将参数11传递给后面then中指定的onFulfilled函数。
2. Promise.reject
1 | Promise.reject(new Error("我错了!")); |
三、Promise的作用
在开发中可能会碰到这样的需求:使用ajax发送A请求,成功后拿到数据,需要把数据传给B请求,那么需要这样编写代码:
1 | let fs = require('fs') |
这段代码之所以看上去很乱,归结其原因有两点:
- 第一是嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。
- 第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的错误处理的方式,明显增加了代码的混乱程度。
既然原因分析出来了,那么问题的解决思路就很清晰了:
- 消灭嵌套调用;
- 合并多个任务的错误处理。
这么说可能有点抽象,不过 Promise 解决了这两个问题。接下来就看看 Promise 是怎么消灭嵌套调用和合并多个任务的错误处理的。
Promise出现之后,代码可以这样写:
1 | let fs = require('fs') |
通过引入 Promise,上面这段代码看起来就非常线性了,也非常符合人的直觉。
1 | readFilePromise('1.json').then(data => { |
Promise 利用了三大技术手段来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡。
四、Promise的方法
Promise常用的方法:then()、catch()、finally()、all()、race()、allSettled()、any()
1. Promise构造函数:Promise(excutor){}
excutor函数:同步执行 (resolve, reject)=>{}
resolve函数:内部定义成功时我们调用的函数 value=>{}
reject函数:内部定义失败时我们调用的函数 reason=>{}
说明:excutor会在Promise内部立即同步回调,异步操作在执行器中执行
2. Promise.prototype.then():(onResolved, onRejected)=>{}
onResolved函数:成功的回调函数 (value)=>{}
onRejected函数:失败的回调函数 (reason)=>{}
说明:指定用于得到成功value的成功回调和用于得到失败reason的失败回调,返回一个新的promise对象
then会接收两个参数(函数),第一个参数会在执行resolve之后触发(还能传递参数),第二个参数会在执行reject之后触发(其实也可以传递参数,和resolve传递参数一样),但是对于Promise中的异常处理,我们建议用catch方法,而不是then的第二个参数。
3. Promise.prototype.catch():(onRejected)=>{}
onRejected函数:失败的回调函数 (reason)=>{}
说明:then()的语法糖,相当于:then(undefined, onRejected)
4. Promise.resolve():(value)=>{}
value: 成功的数据或promise对象
说明:返回一个成功/失败的promise对象
5. Promise.reject():(reason)=>{}
reason:失败的原因
说明:返回一个失败的promise对象
6. Promise.all()
读取两个文件data1.json和data2.json,现在我需要一起读取这两个文件,等待它们全部都被读取完,再做下一步的操作。此时需要用到Promise.all。
当数组中的所有promise的状态都达到resolved时,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。两个promise都为resolved时,all()就成功,否则就失败。
1 | // Promise.all 接收一个包含多个 promise 对象的数组 |
7. Promise.race()
读取两个文件data1.json和data2.json,现在我需要一起读取这两个文件,但是只要有一个已经读取了,就可以进行下一步的操作。此时需要用到Promise.race
如果第一个promise状态是resolved,那race()就直接返回resolved;反之,如果第一个promise变成rejected,那race()就会变成rejected。所有,race()结果取决于响应的第一个promise的状态。
1 | // Promise.race 接收一个包含多个 promise 对象的数组 |
8. Promise.allSettled()
这是ES11新增的特性。
Promise.allSettled的语法及参数跟all类似,不同之处在于通过Promise.allSettled我们可以拿到每个Promise的状态,而不管其是否处理成功。
1 | const resolved = Promise.resolve(2); |
9. ES12新增的 Promise.any()
只要有一个成功就返回fulfilled,如果所有参数Promise实例都为rejected状态,那么any()最终结果就是rejected。
1 | const resolved = Promise.resolve(2); |
10. ES9新增 Promise.prototype.finally()
五、Promise的异常处理
错误处理是所有编程范式都必须要考虑的问题,在使用JavaScript进行异步编程时也不例外。如果我们不做特殊处理,会怎样呢?来看下面的代码,先定义一个会失败的方法。
1 | let fail = () => { |
调用:
1 | console.log(1); |
可以看到打印出了1和2,并在1秒后,获得一个“Uncaught Error”的错误打印,注意观测这个错误的堆栈:
1 | Uncaught Error: fail |
可以看到,其中的 setTimeout (async) 这样的字样,表示着这是一个异步调用抛出的堆栈。但是,“captured”这样的字样也并未打印,因为母方法 fail() 本身的原始顺序执行并没有失败,这个异常的抛出是在回调行为里发生的。 从上面的例子可以看出,对于异步编程来说,使用try…catch是无法捕获错误的。我们需要使用一种更好的机制来捕获并处理可能发生的异常。
Promise 除了支持 resolve 回调以外,还支持 reject 回调,前者用于表示异步调用顺利结束,而后者则表示有异常发生,中断调用链并将异常抛出:
1 | const exe = (flag) => () => new Promise((resolve, reject) => { |
上面的代码中,flag 参数用来控制流程是顺利执行还是发生错误。在错误发生的时候,no 字符串会被传递给 reject 函数,进一步传递给调用链:
1 | Promise.resolve() |
上面的调用链,在执行的时候,第二行就传入了参数 false,它就已经失败了,异常抛出了,因此第三行的 exe 实际没有得到执行,执行结果如下:
1 | false |
这就说明,通过这种方式,调用链被中断了,下一个正常逻辑 exe(true) 没有被执行。 但是,有时候需要捕获错误,而继续执行后面的逻辑,该怎样做?这种情况下就要在调用链中使用 catch 了:
1 | Promise.resolve() |
这种方式下,异常信息被捕获并打印,而调用链的下一步,也就是第四行的 exe(true) 可以继续被执行。将看到这样的输出:
1 | false |
六、Promise 原理
Promise只是对于异步操作代码可读性的一种变化,它并没有改变 JS 异步执行的本质,也没有改变 JS 中存在callback的现象。
值得注意的是,Promise 是用来管理异步编程的,它本身不是异步的,new Promise的时候会立即把executor函数执行,只不过我们一般会在executor函数中处理一个异步操作。如下例子:
1 | let p1 = new Promise(()=>{ |
Promise 采用了回调函数延迟绑定技术,在执行 resolve 函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行。这具体是啥意思呢?我们先来看下面的例子:
1 | let p1 = new Promise((resolve,reject)=>{ |
new Promise的时候先执行executor函数,打印出 1、2,Promise在执行resolve时,触发微任务(其实就是setTimeout(()=>{}, 0)),还是继续往下执行同步任务,执行p1.then时,存储起来两个函数(此时这两个函数还没有执行),然后打印出3,此时同步任务执行完成,最后执行刚刚那个微任务,从而执行.then中成功的方法。
七、Promise的实现
1 | const PENDING = "pending"; |