解决回调地狱的异步操作,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 必知必会(十道题)