期约
同步与异步
同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。这样的执行流程容易分析 程序在执行到代码任意位置时的状态(比如变量的值)。
异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问 一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。 异步操作的例子可以是在定时回调中执行一次简单的数学计算:
1 | let x = 3; |
以往的异步编程模式
在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。
1.异步返回值
2.失败处理
3.嵌套异步回调
如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。 嵌套回调的代码维护起来就是噩梦
期约(promise)
期约基础
ECMAScript 6 新增的引用类型 Promise,可以通过 new 操作符来实例化。创建新期约时需要传入 执行器(executor)函数作为参数
下面的例子使用了一个空函数对象来应付一下解释器:
1 | let p = new Promise(() => {}); |
之所以说是应付解释器,是因为如果不提供执行器函数,就会抛出 SyntaxError。
期约状态机
期约是一个有状态的对象,可能处于如下 3 种状态之一:
待定(pending)
兑现(fulfilled),有时候也称为“解决”,resolved)
拒绝(rejected)
1.在待定状态下,期约可以落定为代表成功的兑现 (fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。
2.不能保证期约必然会脱离待定状态。
3.要组织合理的代码,无论期约解决(resolve)还是拒绝(reject),甚至永远处于待定(pending)状态,都应该具有恰当的行为。
期约两大用途
1.抽象地表示一个异步操作。
期约的状态代表期约是否完成。“待定” 表示尚未开始或者正在执行中。“兑现”表示已经成功完成,而“拒绝”则表示没有成功完成。
假设期约要向服务器发送一个 HTTP 请求。请求返回 200-299 范围内的状态码就足以让期约的状态变为“兑现”。类似地,如果请求返回的状态码不在 200-299 这个范围内, 那么就会把期约状态切换为“拒绝“。
2.期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值。
假设期约向服务器发送一个 HTTP 请求并预定会返回一个 JSON。如果请求返回范围在 200-299 的状态码,则足以让期约的状态变为兑现。此时期约内部就可以收到一个 JSON 字符串。类似地,如果请求返回的状态码不在 200-299 这个范围内,那么就会把期约状态切换为拒绝。此时拒绝的理由可能是一个 Error 对象,包含着 HTTP 状态码及相关错误消息
通过执行函数控制期约状态
1.期约的状态是私有的,所以只能在内部进行操作
2.执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。
3.控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用 resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛出错误。
Promise.resolve()
Promise.resolve() 期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用 Promise.resolve()静态方法,可以实例化一个解决的期约。
这个解决的期约的值对应着传给 Promise.resolve()的第一个参数。使用这个静态方法,实际上可以把任何值都转换为一个期约:
1 | setTimeout(console.log, 0, Promise.resolve()); // Promise : undefined setTimeout(console.log, 0, Promise.resolve(3)); // Promise : 3 |
对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此, Promise.resolve()可以说是一个幂等方法
1 | let p = Promise.resolve(7); |
这个幂等性会保留传入期约的状态。
Promise.reject()
Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误 (这个错误不能通过 try/catch 捕获,而只能通过拒绝处理程序捕获)。
下面的两个期约实例实际上是一样的:
1 | let p1 = new Promise((resolve, reject) => reject()); |
关键在于,Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期 约对象,则这个期约会成为它返回的拒绝期约的理由:
1 | setTimeout(console.log, 0, Promise.reject(Promise.resolve())); |
同步/异步执行的二元性
两种模式下抛出错误的情形:
1 | try { |
1.第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到。
2.这里的同步代码之所以没有捕获期约抛出的错误,是因为它没有通过异步模式捕获错误。从这 里就可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式 的媒介。
3.在前面的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队 列来处理的。因此,try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互 的方式就是使用异步结构——更具体地说,就是期约的方法。
期约的实例方法
实现 Thenable 接口
在 ECMAScript 暴露的异步结构中,任何对象都有一个 then()方法。这个方法被认为实现了 Thenable 接口。
ECMAScript 的 Promise 类型实现了 Thenable 接口。
Promise.prototype.then()
Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个 then()方法接收最多 两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话, 则会在期约分别进入“兑现”和“拒绝”状态时执行。传给then()的任何非函数类型的参数都会被静默忽略。
Promise.prototype.then()方法返回一个新的期约实例:
1 | let p1 = new Promise(() => {}); |
这个新期约实例基于 onResovled 处理程序的返回值构建。换句话说,该处理程序的返回值会通过 Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会 包装上一个期约解决之后的值。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回值 undefined
onRejected 处理程序也与之类似:onRejected 处理程序返回的值也会被 Promise.resolve() 包装。乍一看这可能有点违反直觉,但是想一想,onRejected 处理程序的任务不就是捕获异步错误吗? 因此,拒绝处理程序在捕获错误后不抛出异常是符合期约的行为,应该返回一个解决期约。
Promise.prototype.catch()
Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数: onRejected 处理程序。
传递解决值和拒绝理由
到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理 程序。拿到返回值后,就可以进一步对这个值进行操作。比如,第一次网络请求返回的 JSON 是发送第 二次请求必需的数据,那么第一次请求返回的值就应该传给 onResolved 处理程序继续处理。当然,失 败的网络请求也应该把 HTTP 状态码传给 onRejected 处理程序。
1.在执行函数中,解决的值和拒绝的理由是分别作为 resolve()和 reject()的第一个参数往后传 的。
2.这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一 参数。
1 | let p1 = new Promise((resolve, reject) => resolve('foo')); |
Promise.resolve()和 Promise.reject()在被调用时就会接收解决值和拒绝理由。同样地,它 们返回的期约也会像执行器一样把这些值传给 onResolved 或 onRejected 处理程序:
1 | let p1 = Promise.resolve('foo'); |
拒绝期约与拒绝错误处理
1.拒绝期约类似于 throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。
2.期约可以以任何理由拒绝,包括 undefined,但最好统一使用错误对象。
期约连锁与期约合成
期约连锁
把期约逐个地串联起来。之所以可以这样做,是因为每个期约实例的方 法(then()、catch()和 finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。
1 | let p = new Promise((resolve, reject) => { |
这个实现最终执行了一连串同步任务。没有那么有用。使用 4 个同步函数也可以做到:
1 | (() => console.log('first'))(); |
要真正执行异步任务,可以改写前面的例子,让每个执行器都返回一个期约实例。
这样就可以让每 个后续期约都等待之前的期约,也就是串行化异步任务。
1 | let p1 = new Promise((resolve, reject) => { |
每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。这种结构可以简 洁地将异步任务串行化,解决之前依赖回调的难题。
Promise.all()
1.Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个 可迭代对象,返回一个新期约:
2.如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的 期约也会拒绝:
3.如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:
4.如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期 约不会影响最终期约的拒绝理由。
Promise.race()
1.Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个 方法接收一个可迭代对象,返回一个新期约:
2.Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的 期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约
1 | // 解决先发生,超时后的拒绝被忽略 |
3.如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由。之后再拒绝的期约 不会影响最终期约的拒绝理由。
promise的异常处理(附加)
promise异常会向着链条向后传递
catch能捕获链式上的上一个promise对象的异常
如果使用then,只能捕获ajax这个promise对象的异常
可以注册一个全局处理异常对象事件在window上
而在node当中,需要在process当中注册这个事件
不过还是尽量在代码中明确捕获每一个异常,而不是丢给全局。