ES6中的 Promise
我们都知道,javascript 是一种单线程编程语言,这意味着它在同一个时间,只能一次运行一段代码。当需要完成多个工作任务时,那么 javascript 引擎会将这些任务逐个排序,只有当完成前一个任务项时,才能继续执行下一个任务项,依次进行下去,直到最后一个任务。
javascript 的这种执行模式也被称为 同步模式(sync)。这种模式的特点是运行时间长、容易出现阻塞程序卡死、后续任务必须等待。
同步模式 最常见的表现形式是刷新页面。即当用户刷新页面后,客户端发送请求或处理事件期间,用户必须等待,此时浏览器可能会无响应或者假死,而且其他任务也必须排队执行。
与 同步模式(sync)相对应的是 异步模式(async)。这种模式的特点是运行时间短、多任务同时执行、提高服务器性能。
异步模式 最常见的表现形式是 ajax 请求。即当用户发送ajax请求或处理事件期间,用户可以去做其他事或程序可以执行其他任务,等到请求返回或事件完毕后,通过回调函数的方式再执行相应的操作。
可以看到,对于一些复杂的软件开发,异步模式编程 尤为重要。
在 ES6 之前,常见的异步操作方式主要有三种,即 事件驱动、回调函数、异步操作库。
事件驱动
所谓的事件驱动,也就是通过用户的操作,来触发某些事件的执行、或发送某些请求,类似按需加载。比如说,你在按钮 button 上添加了一个事件,事件的内容是弹出 button 自身的值。那么,只有当你点击 button 后,弹出按钮的值这个事件才会被添加到 工作队列 中,然后执行它。来看下相关代码:
var btn = document.getElementById('btn'); |
这种操作方式意图很明确,从事件本身来说比较简单,适用些基本的交互。
回调函数
在异步编程中,回调函数最值得一说的表现形式就是 ajax 请求,我们通常会在回调函数中处理由ajax请求回来的数据。
假设这里的例子使用 jquery 中 ajax 的请求方式,请求的数据文件为 data.txt,它的内容是 “我是ajax请求会的内容”,那么代码如下:
$.ajax('data.txt', function(data) { |
另外,javascript 提供的定时器方法(setTimeout、setInterval)也能用于 异步编程,即只有当到了某个时间段后,回调函数的内容才会被添加到 工作队列中:
setTimeout(function() { |
回调函数也存在一些问题,比如说,如果各个请求之间存在依赖关系,即后一个请求需要使用前一个请求的结果,或者需要前前一个的请求结果,这样你就得嵌套层层回调,此时便会出现回调地狱:
$.ajax('data1.txt', function(data) { |
这样代码变得很难调试,不能捕获到错误,并且代码层面看上去也不容易理解。
异步操作库
上面说的两种形式都是基于原生javascript,如果需求较复杂,可能实现起来那就没那么容易。于是 javascript 开源社区出现了一些异步操作的库,比较出名的有 jquery 中的 deferred、promise.js、queue.js 等。
其实,无论是使用上面提到的哪种方式,它们都会存在这样或者那样的问题。接下来,我们来看看 ES6 引入的 promise。
一、Promise 的创建以及状态
我们可以通过 Promise 的构造函数来新创建一个 Promise,这个新创建的 Promise 不会立马运行其中的代码,它只是异步操作的占位符,用于在未来的某个时刻触发,或者永远也不触发(出现错误时)。
先来看下它的基本语法:
new Promise(executor); |
Promise 的构造函数接受一个参数,这个参数也被称为 执行器(executor),它是一个函数。该函数包含了初始化 Promise 的相关代码,并且该执行器函数也有两个参数,即:resolve
和 reject
讲这两个参数之前,我们先来说说 Promise 的三个不同状态,它们分别如下:
- pending:Promise 的初始状态
- fulfilled:异步操作成功的状态
- rejected:异步操作失败、错误等状态
新创建的 Promise 是处于 pending
这样一个初始状态,它被认为是 unsettled(未完成)的。
当进行异步操作后,Promise 状态就由初始状态转变成后面两种状态的一种。这个时候,它则被认为是 settled(已完成)的。
Promise 并没有提供相关的属性或者方法用于获取这些状态值,它只是给了我们对于 Promise 中异步操作后状态发生改变的一个感性认识。
这样,我们就可以知道当前 Promise 异步操作后,是哪个状态,后续会进入怎样的操作。
我们用代码来量化下:
var promise = new Promise(function(resolve, reject) { |
通过上面的代码,我们回到之前 执行器 的两个参数:resolve
和 reject
。当 Promise 进行异步操作时,如果出现错误,那么就相当于由 pending
状态进入 rejected
状态。此时,执行器 中就会调用 reject()
这个函数,并将一个错误对象以参数的形式传递给该函数。
倘若 Promise 异步操作成功,那么就相当于由 pending
状态进入 fulfilled
状态 。此时,执行器 中就会调用 resolve()
这个方法,并将异步操作的结果以参数的形式传递给该函数。
二、Promise 原型方法
在了解了 Promise 的相关状态和函数后,我们再来看看 Promise 的原型上有两个方法,即:then()
和 catch()
。
为了节省篇幅以及代码量,我们首先把一个ajax请求封装成一个 Promise,并它赋值给名为 getData
的变量,文章后面提到的 getData
都是此处定义的:
var getData = function() { |
2.1 Promise.prototype.then()
当 Promise 异步操作完,状态改变后,我们可以通过 then()
方法为 Promise 添加对应的回调函数。它的基本语法如下:
Promise.prototype.then(onFulfilled, onRejected) |
该方法返回一个新的 Promise 对象,正因为如此,Promise 也可以采用链式调用的写法。
同时,该方法还可以接收两个参数,第一个为必选,表示 fulfilled 状态下的回调函数。而第二个参数为可选,表示 rejected 状态下的回调函数。
当传递两个参数,则表示同时指定已成功完成和错误的回调函数:
// Fulfilled or Rejected |
当然,你可以只传递一个参数,用于设定已成功完成的回调函数:
// Fulfilled |
或者,你也可以通过下面的形式只指定错误回调函数:
// Rejected |
2.2 Promise.prototype.catch()
当 Promise 异步操作完,状态改变后,并且发生错误时,我们可以通过 catch()
方法为 Promise 添加的回调函数来捕获这个错误。它的基本语法如下:
Promise.prototype.catch(onRejected) |
该方法返回一个新的 Promise 对象,正因为如此,Promise 也可以采用链式调用的写法。
同时,该方法还可以接收一个参数,表示 rejected 状态下的回调函数。下面的代码演示了错误的捕获:
var promise = new Promise(function(resolve, reject) { |
它等价 then 的这种形式:
var promise = new Promise(function(resolve, reject) { |
如前面提到的,因为 then()
和 catch()
方法都是返回一个新的 Promise 对象,所以,我们可以使用链式调用的写法:
var promise = new Promise(function(resolve, reject) { |
同样的,之前的 then()
双参数写法:
getData.then(function(data) { |
也可以改成链式调用的形式:
getData.then(function(data) { |
采用这种写法后,代码逻辑结构变得更清晰,并且如果前面的 then()
方法出错,那么后面的 catch
将捕获这个错误。
上面所介绍的内容,可以总结为 MDN 资料张的一张图:

三、Promise 实例方法
讲完 Promise 的原型方法,我们再来说说他的实例方法。
3.1 Promise.resolve()
Promise.resolve()
方法用于创建一个处于 fulfilled
状态的 Promise。来看下它的基本语法:
Promise.resolve(value); |
以下两种写法等价:
var promise1 = new Promise(function(resolve, reject) { |
我们可以通过回调函数 then
来获取它状态改变后的值:
var promise = Promise.resolve('abc'); |
如果 Promise.resolve()
的参数是一个 Promise,此时返回的结果仍然是这个 Promse,不会对它做任何修改。代码验证如下:
var promisePara = new Promise(function(resolve, reject) { |
如果传入到 Promise.resolve()
的参数是一个具有 then
方法的对象,那么这个对象被称为 thenable
对象,当调用 Promise.resolve()
时,会立即执行 thenable
对象中的 then()
方法,并且返回一个新的 Promise:
var obj = { |
还有一种特殊情况,即 Promise.resolve()
不传入任何参数,此时,该函数就返回一个处于 fulfilled
状态的 Promise。
3.2 Promise.reject()
Promise.resolve()
方法用于创建一个处于 rejected
状态的 Promise。来看下它的基本语法:
Promise.reject(reason); |
以下两种写法等价:
var promise1 = new Promise(function(resolve, reject) { |
与 Promise.resolve()
方法不同,如果只是调用 Promise.reject()
,则会出现报错:
var promise = Promise.reject('error!'); // 错误:Uncaught (in promise) error! |
正确的做法是,需要在其后面添加回调函数 catch()
:
var promise = Promise.reject('error!'); |
3.3 Promise.all()
之前涉及到的方法都是处理单个 Promise 的,倘若要处理多个 Promise 实例,你可以使用 Promise.all()
方法,它的基本语法如下:
Promise.all(iterable); |
该方法接收一个具有迭代属性的列表作为参数,表示需要监听的 Promise 列表,通常这个参数的数据类型是由多个 Promise 组成的数组。比如:
var promise1 = Promise.resolve('a'), |
Promise.all()
返回的值,取决于它所监听的每个 Promise 中的状态函数。
如果 Promise.all()
所监听的 Promise 都处于 fulfilled
状态,即都进入了 resolve
函数,那么 Promise.all()
将返回一个新的、且处于 fulfilled
状态的 Promise 对象。 此时调用这个新的 Promise 对象的 then()
方法,那么被监听的 promise 所返回的值将组成一个数组,这个数组以参数的形式传递了 then()
方法。
有点晕,用代码来说明下:
var promise1 = Promise.resolve('a'), |
上面代码中,因为被监听的 promise1、promise2、promise3 状态都变成了 fulfilled
。于是,Promise.all
返回了一个处于 fulfilled
状态、名为 promise4 的新 Promise 对象。当调用 promise4 的 then()
方法时,promise1、promise2、promise3 所返回的值组成了一个数组,并且以参数的形式传递了 then()
方法,所以最后 value 的数据类型为数组,它的值是 [“a”, “b”, “c”] 。
如果 Promise.all()
所监听的 Promise 有一个处于 rejected
状态,即某一个进入了 reject
函数,那么 Promise.all()
会立即返回一个新的、且处于 rejected
状态的 Promise 对象。 此时调用这个新的 Promise 对象的 then()
方法,什么事也不会发生。而调用 catch()
方法,则它的参数是被监听的 Promise 列表中第一个 rejected
的 Promise 所返回的值。
老规矩,我们还是来看下代码吧。
var promise1 = Promise.resolve('a'), |
由于 promise2 处于 rejected
状态,因此 promise4 中回调函数 then()
的参数是 promise2 的返回值。
3.4 Promise.race()
Promise.race()
的用法和 Promise.all()
类似,都是用于处理一组 Promise 对象。它的基本语法如下:
Promise.race(iterable) |
它的参数也是需要监听的 Promise 列表,通常是一个由 Promise 组成的数组。
race 这个单词从字面上理解,有 比赛、赛跑的意思。或许你已经猜到了,Promise.race()
返回的结果取决于所监听的列表中最先改变状态的(无论是 fulfilled 还是 rejected) 的那个 Promise 。来段演示代码:
var promise1 = Promise.resolve('a'), |
或许这段代码说明不了什么,因为它们执行的顺序本来就是 promise1 -> promise2 -> promise3,当中没有任何延时操作。
再来看另外一段:
var promise1 = new Promise(function(resolve, reject) { |
上面代码中,因为被监听的 Promise 列表改变状态的顺序依次为:promise3(Pending->Rejected)、 promise1(Pending->Fulfilled)、 promise2(Pending->Fulfilled)。因此,promise4 的回调函数取 promise3 返回的结果,即在 catch()
方法中输出 c 。
四、Promise 链式应用
通过链式调用的写法,我们在不同的 Promise 传递数据。
首先来看,如果传递的值是原始的数据类型,如:
var promiseChain = new Promise(function(resolve, reject) { |
上面代码中,promiseChain 执行成功后,将在第一个 then
中返回 Math.sqrt(data)
,它的结果为 16,然后第一个 then
将这个结果值以参数的形式,传递给第二个 then
。接着,第二个 then
又返回 Math.sqrt(data)
,它的结果是 4,然后第二个 then
又把这个结果值以参数的形式传递给第三个 then
,以此类推。
如果 Promise 中的 执行器 使用的是 reject
函数,那么通过 catch
方法,数据仍然会往下传递:
var promiseChain = new Promise(function(resolve, reject) { |
倘若传递的值是 Promise 对象,如:
var promiseChain1 = new Promise(function(resolve, reject) { |
可以看到,我们在不同 promiseChain1 执行成功后,在第一个 then
中返回了 promiseChain2,而 promiseChain2 已经处于 Resolved 状态,并通过 resolve
方法返回了 8,此时,便会把这个值以参数的形式传递给第二个 then
函数,并执行该函数。
如果 promiseChain2 执行器中使用的是 reject
函数,那么又会发生什么呢?来看下代码:
var promiseChain1 = new Promise(function(resolve, reject) { |
此时,第二个 then
将不会被调用。
若链式调用中返回 Promise 中存在 reject
方法,我们需要通过 catch
来传递数据:
var promiseChain1 = new Promise(function(resolve, reject) { |
以上就是 ES6 中 Promise 的相关内容,关于异步编程,听说 ES7 又推出了 Async/Await
方案,有时间再去一探究竟。