解决回调地狱的异步操作,Async 函数 是终极办法,但了解生成器和 Promise 有助于理解 Async 函数原理。由于内容较多,分三部分进行,这是第二部分,介绍 Promise 相关。第一部分介绍 Generator,第三部分介绍 Async 函数。
1)Promise 概述
1.1 概念
Promise是 ES6 引入的一个内建对象,通过它可以创建 Promise 实例。
Promise 实例可以想像成一个占位符,当获取到异步数据后,更新 promise。
这里需要注意的是,异步数据不是直接赋值给 promise 实例,而是赋值给 promise 实例的内部属性[[PromiseValue]],通过 promise 的then()或者catch()方法可以获取到这个值。
这也是 “promise” 名称的由来:承诺将来会给一个值,这个值可能是成功的数据也可能是错误信息,总之,会给一个值。
1.2 基本用法
基本用法包含 promise 创建、获取 promise 完成后的值以及错误捕获,他们三者结合使用才算完成了一次 promise 的完整使用过程。
1.2.1 创建 promise 实例及 executor 函数
通过内建对象Promise(promise 的构造函数)创建 promise 实例,其中,Promise接收一个函数作为参数,这个函数称之为 executor 函数。
Executor 函数本身接收两个函数作为参数,通常命名为resolve和reject。它们接收一个值作为参数,作为 promise 完成之后的值传给 promise 实例的内部属性[[PromiseValue]]。它们当中只能有一个执行,执行之后表示当前 promise 已完成(当然,分别表示两个不同的已完成状态,兑现或拒绝)。
实际开发中我们会把期待得到的数据传给
resolve函数,把错误信息传给reject函数。
另外,Executor 函数在传入Promise时会立即执行。见下面例子:
// executor 函数在传入 Promise 后会立即执行 |
上面输出分别为:111,222,444,555。
分析:
1)executor函数立即执行,所以111先打印出来;
2)resolve此时也立即执行,为了体现这一点,给其传入了立即执行函数,直接打印出了222。实际上此时 promise 已经完成,但我们还无法访问完成后的 promise,需要配合下文讨论的then()访问;
3)紧接着是后面代码执行,打印444;
5)最后打印555。
虽然一直在讨论
resolve,reject执行时过程同resolve,不单独讨论。
到目前为止,我们看到的都是同步代码,那 promise 和异步有什么关系呢?
实际使用中,resolve会将 promise 完成后的数据传给then()方法,而then()中的代码是放在微任务中异步执行的,这是其一;
第二点是,resolve往往不是立即执行,比如请求服务器上的数据,数据请求完成后再执行resolve,这个时候,resolve本身也是异步执行的。
1.2.2 then() 方法
promise 实例拥有then()方法,它设计的目的是 异步获取 promise 完成后 的值。
then()方法接受两个函数作为参数(这两个参数都不是必须),当 promise 完成后会执行它们:
- 第一个参数为称为 success 回调函数,在 promise 执行
resolve函数后执行,同时可以接收resovle的入参作为自己的入参; - 第二个参数称为 failure 回调函数,在 promise 执行
reject函数或者程序发生异常时执行,可以接收reject的入参或者错误信息作为参数。见下面例子:
resolve或者reject传给then()的回调函数的参数,实际就是 promise 的内部属性[[PromiseValue]]的值,[[PromiseValue]]作为 promise 实例内部属性没法直接访问,必须借助then()或者下文将要提到的catch()方法获取。
另外,这个属性很重要,因为 promise 成功兑现后,它上面 保存的通常就是我们需要的异步数据。
这里要注意,函数完成之后才执行then(),并且then()里面的函数是异步执行的,最快在当前事件循环的微任务开始执行时执行,这里有两点要注意:
1)虽然是异步,但仍然是在当前事件循环中执行(微任务在当前事件循环中执行,只是在宏任务之后),所以,会比setTimeout()中的代码先执行(setTimeout()中代码会在下一个事件循环中执行);
2)之所以称之为异步,是因为then()里面的代码会移到当前事件循环的微任务中执行,如果then()之后还有宏任务代码需要执行,微任务要等宏任务中的代码执行完之后才执行。
关于 JS 运行机制更详细的讨论可以参考这篇文章 这一次,彻底弄懂 JavaScript 执行机制
看下面代码:
var p = new Promise((resolve, reject) => { |
上面代码中,我们在 executor 函数中直接执行了resolve,所以then()立即执行,并且把111传给success函数,然后把success函数放到了 当前事件循环的微任务队列中:
上述代码分别输出:333,resolvedValue: 111,222
分析:setTimeout()中代码在下一次事件循环中执行,所以最后输出222;then()中的代码在当前事件循环的微任务中执行,需要等到当前事件循环宏任务中的代码执行完,即console.log(333)执行完再执行,所以先输出333,再输出resolvedValue: 111。
为了分离错误处理的代码,promise 还提供了 catch 方法,见下面用法。
1.2.3 catch() 方法
promise 被拒绝后除了可以在then()中传入第二个参数处理,还可以通过catch()方法处理。
catch()可通过在then()后进行链式调用(加多一个.),作用等同在then()中传入的第二个参数,见下代码:
var p = new Promise((resolve, reject) => { |
执行上述输出:errorInfo: 111。
如果只处理一个 promise,两种方式差不多,至于选择哪一种,看个人喜好。但通过链式调用处理多个 promise 时,用catch更加方便。
但不管怎样,都要选择一种错误处理方式,不然当出现错误时引擎会报错:Uncaught (in promise)
2)Promise 状态及内部属性
Promise 作为异步数据的占位符,在整个生命周期中会有不同状态,这些状态在其内部属性[[PromiseStatus]]中有记录。
2.1 Promise 状态
Promise 整个生命周期状态可分为两个部分:未完成状态和已完成状态,其中,未完成状态只有 pending 一种情况,而已完成状态又分为 fullfilled(已兑现,resolve函数执行)和 rejected(被拒绝,reject函数执行或者 promise 处理时发生异常)两种情况,见下图:

图拍引自 Secrets of the JavaScript Ninja(2nd)
英文已完成状态称为 “resolved states”,后面将要介绍的
[[PromiseStatus]]也有resolved状态值。从生命周期的角度看,如果用英文描述,则 resolved states 包括resolved和rejected两种情况。
另外,promise 一般完成,状态就不能改变。
2.2 Promise 内部属性
Promise 实例创建以后,我们需要关注两个内部属性:
[[PromiseStatus]]:保存当前 promise 的状态,可取值pending,resolved,rejected;[[PromiseValue]]:当 promise 在未完成状态时,其值为undefined,当 promise 完成后,其值为异步数据,或者错误信息。
见下面例子:
1)pending 状态
var p = new Promise((r, j) => {}); |
此时,p 处于pending状态,内部属性值如下:
// 实际代码中这样写无效,这里只是延时,但可以在浏览器 console 中看到 |
2)resolved 状态
var p = new Promise((r, j) => { |
此时,p 处于resolved状态,内部属性值如下:
p { |
3)rejected 状态
var p = new Promise((r, j) => { |
此时,p 处于rejected状态,内部属性值如下:
p { |
这里没有捕获错误,所以会报错
Uncaught (in promise) 111,不过,此处不是我们关注的重点
2.3 关于 rejected 状态
两种情况可能导致 promise 进入 rejected 状态:
- 显式拒绝:在 promise 的 executor 函数中执行
reject(); - 隐式拒绝:处理 promise 的时候程序发生了异常。
显式拒绝的情况我们在上文已看,下面我们看个隐式拒绝的例子:
// promise 隐式拒绝 |
因为a 没有定义,所以a++执行时报错了,promise 被拒绝,然后我们在catch()方法中捕捉到了错误,此时p内部属性描述如下:
p { |
2.4 来一道习题
关于这个习题的分析看附录部分。
console.log("1"); |
3)使用 Promise
3.1 用 promise 封装 ajax
这部分将用 promise 封装 ajax 的 GET 请求,为接下来的链式调用 promise 做准备。
最简单的 ajax 请求,没有考虑兼容性,请求错误处理等,主要说明 promise 应用
// 使用 promise 封装 ajax |
使用通过 promise 封装过的 ajax 就简单了:
promiseAjax("someURL") |
promise 在请求大量异步数据时优势会体现的更明显。
3.2 链式调用 Promise
链式调用之所以能够实现是因为then()和catch()会 返回一个新的 promise 对象。
链式调用有几个特点:
- 当前面的 promise 完成后,才会执行后面的 promise;
- 如果
then()或catch()没有显式 return promise,则当前返回的 promise 默认是兑现的; - 如果
then()或catch()显式 return promise,则当前返回的 promise 是兑现还是拒绝取决于then()或catch()显式 return promise 的状态。
基于上,以then()为例,可能出现几种写法:
3.2.1 没显式的 return 语句
then()里面没有显式返回任何代码,如下:
var p = new Promise((r, j) => { |
没有显式的return语句,默认返回已完成且[[PromiseValue]]值为undefined的 promise
上面代码等同于:
var p = new Promise((r, j) => { |
Promise.resolve() 返回一个完成状态的 promise,并且
p1.then()也返回一个已完成的 promise,但没有使用
此时 p、p1 值如下:
p { |
3.2.2 显式 return 非 promise
如果在then()中显式return非 promise,返回的数据就是当前 promise 兑现后的[[PromiseValue]]的值:
var p = new Promise((r, j) => { |
以上代码相当于:
var p = new Promise((r, j) => { |
此时 p、p1 的值如下:
p { |
NOTICE:
返回错误对象也是相当于返回兑现的 promise,如下:
var p = new Promise((r, j) => { |
此时打印的是then:出错了,因为相当于() => Promise.resolve(new Error('出错了')),此时是兑现的 promise,只不过显式传递了一个“出错了”的数据给当前兑现后的 promise 的[[PromiseValue]]。
3.2.3 显式 return 未完成的 promise
但在then()中显式返回未完成的 promise 时,当前then()返回的 promise 是兑现还是拒绝取决于显式返回的未完成的 promise 的状态。
如果显式返回未完成的 promise 最终兑现了,当前 promise 是兑现状态,否则为拒绝状态。这是实际工作中常见的场景,比如链式调用相互依赖的异步数据, 3.2.4 中我们将看到这个例子,现在看下基本用法。
最终兑现的状态和前面的显式返回Promise.resolve()相同,我们看个最终被拒绝的例子:
var p = new Promise((r, j) => { |
此时会打印出 拒绝,因为显式返回未完成的 promise 最终被拒绝了。相当于:
var p = new Promise((r, j) => { |
NOTICE
如果当前返回的是被拒绝的 promise,则后面的所有then()都不执行,直到遇到第一个catch(),执行catch()里面的代码,并且当前catch()也会返回一个 promise。
但
catch()返回的 pormise 一般不用,只在链式调用的最后放个catch()捕捉错误就好了,如下:
var p = new Promise((r, j) => { |
上面返回 拒绝 和 兑现3。
3.2.4 一个真实的例子
如果几个异步数据间存在依赖关系,可以使用 promise 的链式调用方式,比如:
需求:请求 URL1 得到 data1;请求 URL2 得到 data2,但 URL2 = data1[0].url2;请求 URL3 得到 data3,但 URL3 = data2[0].url3。
使用 promise 链式调用可以这样写代码:
// promiseAjax 在 3.1 中有定义 |
3.3 Promise 其他用法
Promise 还有两个常用功能Promise.all()和Promise.race,它们都用于一次处理多个 promise,不同点是:
Promise.all()可以一次处理多个 promise,我们 不需要关心哪个先完成,全部兑现后后统一返回,但任何一个 promise 被拒绝都会导致整个 promise 被拒绝;- 使
Promise.race()时我们也 不需要关心执行顺序,但任何一个 promise 完成就会立即返回这个完成的 promise。
它们的不同点主要体现在 promise 兑现后传递给then()的数据:Promise.all()返回的是所有 promise 兑现后组成的数组数据,而Promise.race()返回的是 最先完成的那一个 promise 返回的数据。见下面的代码例子:
1)Promise.all()
Promise.all([ |
2)promise.rase()
Promise.race([ |
4)promise 的实现
限于篇幅,将在另一篇文章中专门写 promise 的原理及实现。
网上有很多类似文章了,自己写一遍加深印象。
5)其他说明
相比使用回调函数,promise 已经使代码得到了很大改善, 但相比同步代码还是看着有些复杂,比如,链式调用那部分,代码看上去还是有些混乱。
那能不能用写同步代码的方式请求异步数据呢?可以的,这就是第三部分将要介绍的 Async 函数。
那是不是说 promise 就没用了呢?并不是,Async 函数实际是 生成器 + promise 的语法糖,只有理解了生成器和 promise 的原理,才能更好的理解 Async 函数。并且,现在很多 Web API 甚至库(比如 axios)都是经过 promise 封装的,熟悉 promise 的使用及原理有助于理解和 promise 相关的其他代码。
附录
2.4 答案及分析分析
2.4 的习题分析,见注释:
console.log("1"); // 1 |
参考资料
【1】[美]JOHN RESIG,BEAR BIBEAULT and JOSIP MARAS 著(2016),Secrets of the JavaScript Ninja (Second Edition),第 6 章 promise 部分,Manning Publications Co.
【2】[美]Nicholas C. Zakas 著,刘振涛 译(2017),深入理解 ES6,p86~p97,电子工业出版社
【3】Promise 必知必会(十道题)