异步函数 | Promise基础和实例方法
ES6增加了对Promise/A+规范的完善支持,即Promise实例
Promise基础
1 | new Promise(executor) |
Promise可以通过new操作符来实例化,创建新Promise的时候需要传入执行器(executor)函数作为参数
1 | let p = new Promise(() => {}); // 这里使用一个空函数进行模拟 |
Promise状态
Promise是一个有状态的对象,Promise可能处于如下状态
- 待定(pending)
- 兑现(fulfilled或者rosolved)
- 拒绝(rejected)
Promise的初始状态是Pending,在Pending状态下,Promise的状态可变为fulfilled或者rejected
Promise的状态一经改变不可逆!!!
Promise的状态是私有的,是不能直接通过JavaScript检测到的(这主要是为了避免根据读取到的Promise状态,以同步方式处理Promise对象)
Promise的也不能为外部JavaScript代码修改(Promise故意将异步行为封装起来,从而隔离外部的同步代码)
Promise用途
(1)改变状态
Promise可以用于抽象地表示一个异步操作,Promise的状态可以表示Promise是否完成,Promise的状态一经改变不可撤销和逆转
1 | let p = new Promise((resolve, reject) => { |
为了避免Promise卡在某个状态,可以添加一个定时器退出功能
1 | let p = new Promise((resolve, reject) => { |
因为期约的状态只能改变一次,所以这里的超时拒绝逻辑中可以放心地设置让期约处于待定状态的最长时间
如果执行器中的代码在超时之前已经解决或拒绝,那么超时回调再尝试拒绝也会静默失败(Nice!)
(2)回调处理
Promise封装的异步操作会实际生成某个值,而程序期待Promise状态改变时可以访问这个值,相应地,如果Promise被拒绝,程序就会期待Promise状态改变时可以拿到拒绝的理由
每个Promise只要状态改变为fulfilled,就会有一个私有的内部值(value)
每个Promise只要状态改变为rejected,就会有一个私有的内部理由(reason)
无论是值还是理由,都是包含原始值或对象的不可修改的引用,二者都是可选的,而且默认值为undefined
在Promise到达某个落定状态时执行的异步代码始终会收到这个值或理由
通过执行函数控制Promise状态
因为Promise的状态是私有的,所以只能在内部进行操作, 内部操作在Promise的执行器函数中完成
执行器函数主要有两大职责:
- 初始化Promise的异步行为
- 控制状态的最终转换【调用resolve()和reject()实现】
1 | let p1 = new Promise((resolve, reject) => resolve()); |
- 执行器函数是同步执行的,因为执行器函数是Promise的初始化状态
1 | new Promise(() => setTimeout(() => { |
- 添加setTimeout可以推迟切换状态
1 | let p = new Promise((resolve, reject) => setTimeout(resolve, 1000)); |
Promise静态方法
(1)Promise.resolve()
Promise.resolve()可以实例化一个解决的Promise
1 | // p1 和 p2 是等价的 |
Promise.resolve()的参数对应着解决的Promise的值,Promise.resolve()可以把任何值都转换为一个Promise,传入的参数可能会有如下情况
- 定值 => 解析或者包装后的Promise对象
- Promise对象 => Promise对象本身(相当于一个空包装)【幂等】
- thenable => 返回的Promise会跟随着thenable对象,采用它的最终状态
1
2
3
4
5
6
7
8
9
10
11// 定值
let p1 = Promise.resolve(2);
console.log(p1); // Promise {<fulfilled>: 2}
// 多余的参数会忽略
let p2 = Promise.resolve(4);
console.log(p2); // Promise {<fulfilled>: 4}
// 空包装
let p3 = Promise.resolve(p2);
console.log(p3 === p2); // truePromise.resolve()可以包装任何非Promise的值,包括错误对象,并将其转换为解决的Promise
1 | // 可能会导致不符合预期的行为 |
- Promise.resolve()是一个幂等方法
1 | let p = Promise.resolve(7); |
(2)Promise.reject()
Promise.reject()可以实例化一个拒绝的Promise并抛出一个异步错误(这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获)
1 | // 下面两个实例等价 |
Promise.reject()的参数对应着拒绝的Promise的理由
1
2let p1 = Promise.rereject(3);
console.log(p1); // Promise {<rejected>: 3}Promise.reject()不是一个幂等方法,如果给它传一个Promise,这个Promise会成为拒绝的理由
1
2
3
4
5let p1 = Promise.resolve(3);
let p2 = Promise.reject(p1);
console.log(p2); // Uncaught (in promise) Promise {<fulfilled>: 3}
Promise同步、异步执行的二元性
1 | try { |
第二个try/catch没有捕获到错误是因为它没有通过异步模式捕获错误,这是什么意思呢?前面说过:Promise的状态是私有的,是不能直接通过JavaScript检测到的(这主要是为了避免根据读取到的Promise状态,以同步方式处理Promise对象),拒绝Promise的错误并没有抛出到同步代码的线程里,所以try/catch没有捕获到错误,准确地说,要捕获错误就需要用Promise的方法【下面就开始介绍Promise的方法】
Promise的实例方法
实现Thenable接口
在ECMAScript 暴露的异步结构中,任何对象都有一个then()方法,这个方法被认为实现了Thenable 接口
1 | class MyThenable { |
Promise.prototype.then()
Promise.then()返回一个新的Promise实例
Promise.then()最多接收两个参数(可选):onResloved处理程序 和 onRejected处理程序,分别在Promise进入“兑现”和“拒绝”状态时执行
1 | function onResolved(id) { |
因为Promise的状态不可撤销,所以这两个方法一定是互斥的
- 传给then()的任何非函数类型的参数都会被忽略
- 如果只想提供onRejected参数,那么就要在onResolved的位置写上null或者undefined
(1)onResolve处理程序
Promise.then()返回一个新的Promise实例,这个新Promise的实例是基于onResolved处理程序的返回值构建的,也就是说,该处理程序的返回值会通过Promise.resolve()去包装来生成新的Promise实例
那么onResolve()处理程序在不同情况下,Promise新实例的情况也不同,主要分为以下几种情况:
没有提供onResolve处理程序 => 会沿用上一个Promise
提供onResolved处理程序但是没有显式的返回语句 => Promise.resolve()会包装默认返回值为undefined
提供onResolved处理程序且有有显式的返回语句 => Promise.resolve()会包装这个值【根据不同值的情况,具体之前讲过了,可以看Promise.resolve()那块】
1 | let p1 = Promise.resolve(3); // 此时p1 为 Promise: {<fulfilled>: 3} |
onResolved
处理程序遇到错误,分为两种情况抛出异常 => 返回拒绝的
Promise
返回错误值 => 不会拒绝而是会把错误值包装成一个解决的
Promise
1 | let p1 = Promise.resolve(1); |
(2)onReject处理程序
onRejected
处理程序返回的值也会被Promise.resolve()
包装,在捕获错误后不抛出异常,而是返回一个解决的Promise
onRejeced
处理程序在不同情况下,Promise
新实例的情况也不同,主要分为以下几种情况:- 没有提供onRejected处理程序 => 会沿用上一个Promise
- 提供onRejected处理程序但是没有显式的返回语句 => Promise.resolve()会包装默认返回值为undefined
- 提供onRejected处理程序且有有显式的返回语句 =>Promise.resolve()会包装这个原因
1 | let p1 = Promise.reject(3); // 此时p1 为 Promise: {<rejected>: 3} |
Promise.prototype.catch()
Promise.prototype.catch()
方法用于给期约添加拒绝处理程序
这个方法只接收一个参数:onRejected
处理程序
事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null, onRejected)
1 | let p1 = Promise.reject(); |
Promise.catch()返回一个新的Promise实例
1 | let p1 = new Promise(() => {}); |
在返回新期约实例方面,Promise.prototype.catch()
的行为与Promise.prototype.then()
的onRejected
处理程序是一样的
Promise.prototype.finally()
Promise.prototype.finally()方法用于给期约添加onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行
- onFinally 处理程序无法知道Promise的状态是fulfilled还是rejected,所以这个方法主要用于清理代码
1 | let p1 = Promise.resolve(); |
- Promise.prototype.finally()返回一个新的Promise实例
1 | let p1 = Promise.resolve(); |
- onFinally 处理程序和 onResolved和onRejected不同,因为onFinally被设计为一个状态无关的方法,所以在大多数情况下它将表现为父Promise的传递
1 | let p1 = Promise.resolve('foo'); |
- 如果返回的是一个待定的
Promise
,或者onFinally
处理程序抛出了错误(显式抛出或返回了一个拒绝期约),则会返回相应的期约(待定或拒绝)
1 | let p1 = Promise.resolve('foo'); |
非重入Promise方法
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行
跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行
即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的
这个特性由JavaScript 运行时保证,被称为“非重入”(non-reentrancy)特性
感觉这部分只要搞清楚Even Loop就可以了,所以这里略过~
邻近处理程序的执行顺序
如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行
无论是then()、catch()还是finally()添加的处理程序都是如此
这个执行顺序主要是遵循了队列的先进先出原则,掠过~
传递解决值和拒绝理由
到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理程序
拿到返回值后,就可以进一步对这个值进行操作
在执行函数中,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的,然后,这些值又会传给它们各自的处理程序,作为onResolved 或onRejected 处理程序的唯一参数
把这张图理顺~问题不大
拒绝Promise和拒绝错误处理
拒绝期约类似于throw()
表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理
- 在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由
1 | let p1 = new Promise((resolve, reject) => reject(Error('foo'))); |
期约可以以任何理由拒绝,包括
undefined
,但最好统一使用错误对象,这样做主要是因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,而这些信息对调试是非常关键的异步错误只能通过异步的onRejected处理程序进行处理【还记得之前说的:Promise的状态是私有的】
1 | // 不正确 |
then()
和catch()
的onRejected
处理程序在语义上相当于try/catch
,出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行,为此,onRejected 处理程序的任务应该是在捕获异步错误之后返回一个解决的期约
1 | console.log('begin synchronous execution'); |