我们都知道,javascript 是一种单线程编程语言,这意味着它在同一个时间,只能一次运行一段代码。当需要完成多个工作任务时,那么 javascript 引擎会将这些任务逐个排序,只有当完成前一个任务项时,才能继续执行下一个任务项,依次进行下去,直到最后一个任务。

javascript 的这种执行模式也被称为 同步模式(sync)。这种模式的特点是运行时间长、容易出现阻塞程序卡死、后续任务必须等待。

同步模式 最常见的表现形式是刷新页面。即当用户刷新页面后,客户端发送请求或处理事件期间,用户必须等待,此时浏览器可能会无响应或者假死,而且其他任务也必须排队执行。

与 同步模式(sync)相对应的是 异步模式(async)。这种模式的特点是运行时间短、多任务同时执行、提高服务器性能。

异步模式 最常见的表现形式是 ajax 请求。即当用户发送ajax请求或处理事件期间,用户可以去做其他事或程序可以执行其他任务,等到请求返回或事件完毕后,通过回调函数的方式再执行相应的操作。

可以看到,对于一些复杂的软件开发,异步模式编程 尤为重要。

在 ES6 之前,常见的异步操作方式主要有三种,即 事件驱动、回调函数、异步操作库。

事件驱动

所谓的事件驱动,也就是通过用户的操作,来触发某些事件的执行、或发送某些请求,类似按需加载。比如说,你在按钮 button 上添加了一个事件,事件的内容是弹出 button 自身的值。那么,只有当你点击 button 后,弹出按钮的值这个事件才会被添加到 工作队列 中,然后执行它。来看下相关代码:

var btn = document.getElementById('btn');

btn.onclick = function() {
alert(this.value);
};

这种操作方式意图很明确,从事件本身来说比较简单,适用些基本的交互。

回调函数

在异步编程中,回调函数最值得一说的表现形式就是 ajax 请求,我们通常会在回调函数中处理由ajax请求回来的数据。

假设这里的例子使用 jquery 中 ajax 的请求方式,请求的数据文件为 data.txt,它的内容是 “我是ajax请求会的内容”,那么代码如下:

$.ajax('data.txt', function(data) {
console.log(data);
});

console.log('不用等ajax操作');

// "不用等ajax操作"
// "我是ajax请求会的内容"

另外,javascript 提供的定时器方法(setTimeout、setInterval)也能用于 异步编程,即只有当到了某个时间段后,回调函数的内容才会被添加到 工作队列中:

setTimeout(function() {
console.log('1s后输出');
}, 1000);

console.log('不用等延时操作');

回调函数也存在一些问题,比如说,如果各个请求之间存在依赖关系,即后一个请求需要使用前一个请求的结果,或者需要前前一个的请求结果,这样你就得嵌套层层回调,此时便会出现回调地狱:

$.ajax('data1.txt', function(data) {
var data1 = data;

//...

$.ajax('data2.txt', function(data) {
var data2 = data;

//...

$.ajax('data3.txt', function(data) {
var data3 = data;

//...

$.ajax('data4.txt', function(data) {
var data4 = data;

//...
//...
});
});
});
});

这样代码变得很难调试,不能捕获到错误,并且代码层面看上去也不容易理解。

异步操作库

上面说的两种形式都是基于原生javascript,如果需求较复杂,可能实现起来那就没那么容易。于是 javascript 开源社区出现了一些异步操作的库,比较出名的有 jquery 中的 deferred、promise.js、queue.js 等。

其实,无论是使用上面提到的哪种方式,它们都会存在这样或者那样的问题。接下来,我们来看看 ES6 引入的 promise。

一、Promise 的创建以及状态

我们可以通过 Promise 的构造函数来新创建一个 Promise,这个新创建的 Promise 不会立马运行其中的代码,它只是异步操作的占位符,用于在未来的某个时刻触发,或者永远也不触发(出现错误时)。

先来看下它的基本语法:

new Promise(executor);

// 或者
new Promise(function(resolve, reject) { ... });

Promise 的构造函数接受一个参数,这个参数也被称为 执行器(executor),它是一个函数。该函数包含了初始化 Promise 的相关代码,并且该执行器函数也有两个参数,即:resolvereject

讲这两个参数之前,我们先来说说 Promise 的三个不同状态,它们分别如下:

  • pending:Promise 的初始状态
  • fulfilled:异步操作成功的状态
  • rejected:异步操作失败、错误等状态

新创建的 Promise 是处于 pending 这样一个初始状态,它被认为是 unsettled(未完成)的。

当进行异步操作后,Promise 状态就由初始状态转变成后面两种状态的一种。这个时候,它则被认为是 settled(已完成)的。

Promise 并没有提供相关的属性或者方法用于获取这些状态值,它只是给了我们对于 Promise 中异步操作后状态发生改变的一个感性认识。

这样,我们就可以知道当前 Promise 异步操作后,是哪个状态,后续会进入怎样的操作。

我们用代码来量化下:

var promise = new Promise(function(resolve, reject) {

// 异步操作出错
if (error) {
reject(error);
return;
}

// 异步操作成功
resolve(data);
});

通过上面的代码,我们回到之前 执行器 的两个参数:resolvereject。当 Promise 进行异步操作时,如果出现错误,那么就相当于由 pending 状态进入 rejected 状态。此时,执行器 中就会调用 reject() 这个函数,并将一个错误对象以参数的形式传递给该函数。

倘若 Promise 异步操作成功,那么就相当于由 pending 状态进入 fulfilled 状态 。此时,执行器 中就会调用 resolve() 这个方法,并将异步操作的结果以参数的形式传递给该函数。

二、Promise 原型方法

在了解了 Promise 的相关状态和函数后,我们再来看看 Promise 的原型上有两个方法,即:then()catch()

为了节省篇幅以及代码量,我们首先把一个ajax请求封装成一个 Promise,并它赋值给名为 getData 的变量,文章后面提到的 getData 都是此处定义的:

var getData = function() {
var promise = new Promise(function(resolve, reject) {
$.ajax({
url: 'data.txt',
success: function(data) {
resolve(data);
},
error: function(error) {
reject(error);
}
});
});

return Promise;
};

2.1 Promise.prototype.then()

当 Promise 异步操作完,状态改变后,我们可以通过 then() 方法为 Promise 添加对应的回调函数。它的基本语法如下:

Promise.prototype.then(onFulfilled, onRejected)

该方法返回一个新的 Promise 对象,正因为如此,Promise 也可以采用链式调用的写法。

同时,该方法还可以接收两个参数,第一个为必选,表示 fulfilled 状态下的回调函数。而第二个参数为可选,表示 rejected 状态下的回调函数。

当传递两个参数,则表示同时指定已成功完成和错误的回调函数:

// Fulfilled or Rejected
getData.then(function(data) {
console.log(data);
}, function(error) {
console.error(error);
});

当然,你可以只传递一个参数,用于设定已成功完成的回调函数:

// Fulfilled
getData.then(function(data) {
console.log(data);
});

或者,你也可以通过下面的形式只指定错误回调函数:

// Rejected
getData.then(null, function(error) {
console.error(error);
});

2.2 Promise.prototype.catch()

当 Promise 异步操作完,状态改变后,并且发生错误时,我们可以通过 catch() 方法为 Promise 添加的回调函数来捕获这个错误。它的基本语法如下:

Promise.prototype.catch(onRejected)

该方法返回一个新的 Promise 对象,正因为如此,Promise 也可以采用链式调用的写法。

同时,该方法还可以接收一个参数,表示 rejected 状态下的回调函数。下面的代码演示了错误的捕获:

var promise = new Promise(function(resolve, reject) {
throw new Error('出错了!');
});

promise.catch(function(error) {
console.log(error.message); // "出错了!"
});

它等价 then 的这种形式:

var promise = new Promise(function(resolve, reject) {
throw new Error('出错了!');
});

promise.then(null, function(error) {
console.log(error.message); // "出错了!"
});

如前面提到的,因为 then()catch() 方法都是返回一个新的 Promise 对象,所以,我们可以使用链式调用的写法:

var promise = new Promise(function(resolve, reject) {
throw new Error('出错了!');
});

promise.catch(function(error) {
console.log(error.message); // "出错了!"
throw new Error('又出错了!');
}).catch(function(error) {
console.log(error.message); // "又出错了!"
});

同样的,之前的 then() 双参数写法:

getData.then(function(data) {
console.log(data);
}, function(err) {
console.error(err);
});

也可以改成链式调用的形式:

getData.then(function(data) {
console.log(data);
}).catch(function(error) {
console.error(error);
});

采用这种写法后,代码逻辑结构变得更清晰,并且如果前面的 then() 方法出错,那么后面的 catch 将捕获这个错误。

上面所介绍的内容,可以总结为 MDN 资料张的一张图:

三、Promise 实例方法

讲完 Promise 的原型方法,我们再来说说他的实例方法。

3.1 Promise.resolve()

Promise.resolve() 方法用于创建一个处于 fulfilled 状态的 Promise。来看下它的基本语法:

Promise.resolve(value);

以下两种写法等价:

var promise1 = new Promise(function(resolve, reject) {
resolve('abc');
});

// 等价于
var promise2 = Promise.resolve('abc');

我们可以通过回调函数 then 来获取它状态改变后的值:

var promise = Promise.resolve('abc');

promise.then(function(data) {
console.log(data); // "abc"
});

如果 Promise.resolve() 的参数是一个 Promise,此时返回的结果仍然是这个 Promse,不会对它做任何修改。代码验证如下:

var promisePara = new Promise(function(resolve, reject) {
resolve('012');
});
promise = Promise.resolve(promisePara);

promise.then(function(data) {
console.log(data);
});

console.log(promise === promisePara);

// true
// "012"

如果传入到 Promise.resolve() 的参数是一个具有 then 方法的对象,那么这个对象被称为 thenable 对象,当调用 Promise.resolve() 时,会立即执行 thenable 对象中的 then() 方法,并且返回一个新的 Promise:

var obj = {
then: function() {
console.log('The obj is thenable object!');
}
};

var promise = Promise.resolve(obj); // "The obj is thenable object!"

console.log(promise); // Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

还有一种特殊情况,即 Promise.resolve() 不传入任何参数,此时,该函数就返回一个处于 fulfilled 状态的 Promise。

3.2 Promise.reject()

Promise.resolve() 方法用于创建一个处于 rejected 状态的 Promise。来看下它的基本语法:

Promise.reject(reason);

以下两种写法等价:

var promise1 = new Promise(function(resolve, reject) {
reject('error!');
});

// 等价于
var promise2 = Promise.reject('error!');

Promise.resolve() 方法不同,如果只是调用 Promise.reject(),则会出现报错:

var promise = Promise.reject('error!'); // 错误:Uncaught (in promise) error!

正确的做法是,需要在其后面添加回调函数 catch()

var promise = Promise.reject('error!');

promise.catch(function(error) {
console.log(error); // "error!"
});

3.3 Promise.all()

之前涉及到的方法都是处理单个 Promise 的,倘若要处理多个 Promise 实例,你可以使用 Promise.all() 方法,它的基本语法如下:

Promise.all(iterable);

该方法接收一个具有迭代属性的列表作为参数,表示需要监听的 Promise 列表,通常这个参数的数据类型是由多个 Promise 组成的数组。比如:

var promise1 = Promise.resolve('a'),
promise2 = Promise.resolve('b'),
promise3 = Promise.resolve('c');

var promise4 = Promise.all([promise1, promise2, promise3]);

Promise.all() 返回的值,取决于它所监听的每个 Promise 中的状态函数。

如果 Promise.all() 所监听的 Promise 都处于 fulfilled 状态,即都进入了 resolve 函数,那么 Promise.all() 将返回一个新的、且处于 fulfilled 状态的 Promise 对象。 此时调用这个新的 Promise 对象的 then() 方法,那么被监听的 promise 所返回的值将组成一个数组,这个数组以参数的形式传递了 then() 方法。

有点晕,用代码来说明下:

var promise1 = Promise.resolve('a'),
promise2 = Promise.resolve('b'),
promise3 = Promise.resolve('c');

var promise4 = Promise.all([promise1, promise2, promise3]);

promise4.then(function(value) {
console.log(Array.isArray(value), value); // true ["a", "b", "c"]
});

上面代码中,因为被监听的 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 = Promise.reject('b'),
promise3 = Promise.reject('c');

var promise4 = Promise.all([promise1, promise2, promise3]);

promise4.then(function(value) {
console.log(Array.isArray(value), value); // 不执行
}).catch(function(error) {
console.log(typeof error, error); // string "b"
});

由于 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'),
promise2 = Promise.reject('b'),
promise3 = Promise.reject('c');

var promise4 = Promise.race([promise1, promise2, promise3]);

promise4.then(function(value) {
console.log(typeof value, value); // string "a"
}).catch(function(error) {
console.log(error); // 不执行
});

或许这段代码说明不了什么,因为它们执行的顺序本来就是 promise1 -> promise2 -> promise3,当中没有任何延时操作。

再来看另外一段:

var promise1 =  new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('a');
}, 100);
}),
promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('a');
}, 1000);
}),
promise3 = Promise.reject('c');

var promise4 = Promise.race([promise1, promise2, promise3]);

promise4.then(function(value) {
console.log(typeof value, value); // 不执行
}).catch(function(error) {
console.log(typeof error, error); // string "c"
});

上面代码中,因为被监听的 Promise 列表改变状态的顺序依次为:promise3(Pending->Rejected)、 promise1(Pending->Fulfilled)、 promise2(Pending->Fulfilled)。因此,promise4 的回调函数取 promise3 返回的结果,即在 catch() 方法中输出 c 。

四、Promise 链式应用

通过链式调用的写法,我们在不同的 Promise 传递数据。

首先来看,如果传递的值是原始的数据类型,如:

var promiseChain = new Promise(function(resolve, reject) {
resolve(256);
});

promiseChain.then(function(data) {
console.log(data); // 256
return Math.sqrt(data);
}).then(function(data) {
console.log(data); // 16
return Math.sqrt(data);
}).then(function(data) {
console.log(data); // 4
});

上面代码中,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) {
reject(256);
});

promiseChain.catch(function(data) {
console.log(data); // 256
return Math.sqrt(data);
}).then(function(data) {
console.log(data); // 16
return Math.sqrt(data);
}).then(function(data) {
console.log(data); // 4
});

倘若传递的值是 Promise 对象,如:

var promiseChain1 = new Promise(function(resolve, reject) {
resolve(256);
});

var promiseChain2 = new Promise(function(resolve, reject) {
resolve(8);
});

promiseChain1.then(function(data) {
console.log(data); // 256
return promiseChain2;
}).then(function(data) {
console.log(data); // 8
});

可以看到,我们在不同 promiseChain1 执行成功后,在第一个 then 中返回了 promiseChain2,而 promiseChain2 已经处于 Resolved 状态,并通过 resolve 方法返回了 8,此时,便会把这个值以参数的形式传递给第二个 then 函数,并执行该函数。

如果 promiseChain2 执行器中使用的是 reject 函数,那么又会发生什么呢?来看下代码:

var promiseChain1 = new Promise(function(resolve, reject) {
resolve(256);
});

var promiseChain2 = new Promise(function(resolve, reject) {
reject(8);
});

promiseChain1.then(function(data) {
console.log(data); // 256
return promiseChain2;
}).then(function(data) {
console.log(data); // 出错,不会被调用
});

此时,第二个 then 将不会被调用。

若链式调用中返回 Promise 中存在 reject 方法,我们需要通过 catch 来传递数据:

var promiseChain1 = new Promise(function(resolve, reject) {
resolve(256);
});

var promiseChain2 = new Promise(function(resolve, reject) {
reject(8);
});

var promiseChain3 = new Promise(function(resolve, reject) {
resolve(2);
});

promiseChain1.then(function(data) {
console.log(data); // 256
return promiseChain2;
}).catch(function(data) {
console.log(data); // 8
return promiseChain3;
}).then(function(data) {
console.log(data); // 2
});

以上就是 ES6 中 Promise 的相关内容,关于异步编程,听说 ES7 又推出了 Async/Await 方案,有时间再去一探究竟。