- 最早接触
Promise
概念是在创业公司,同事引入的Promise
第三方库,用在用Swift
写的工程中。 - 异步编程,用
block
已经是主流,分为成功、失败、最终三个分支。如果嵌套,那么将一层一层地嵌套进去。在真正执行的时候,“从内向外”一层一层执行。“从外到内”写,但是“从内到外”执行,并且还是一层层嵌套,这个不符合人的思维习惯。Promise
就是将这种情况统一调整为“从左到右”,书写和执行都一样。并且把一层层的嵌套修改为一连串的点点点函数调用,这个“很人性化”。 - 在函数式编程中,
curry
和compose
操作都是“从右向左”的,而Monad
就是将操作改为“很人性化”的“从左向右”,书写和执行都是。并且还可以把嵌套flatMap
一下,“铺平”,从而形成一连串点点点函数调用。 -
Promise
和Monad
针对的领域不同,不过都达到了相同的目标(点点点函数链式调用,从左向右书写和执行)。可以把Promise
看成是一种专门用于异步领域的Monad
什么是Promise
-
Promise
是抽象异步处理对象以及对其进行各种操作的组件。 - 回调函数是处理异步过程的常用方式,传给回调函数的参数为(
error
对象, 执行结果)组合。 -
Node.js
等规定在JavaScript
的回调函数的第一个参数为Error
对象,这也是它的一个惯例。 - 而
Promise
则是把类似的异步处理对象和处理规则进行规范化, 并按照采用统一的接口来编写,而采取规定方法之外的写法都会出错。
基本概念
- 要想创建一个
promise
对象、可以使用new
来调用Promise
的构造器来进行实例化。
var promise = new Promise(function(resolve, reject) {
// 异步处理
// 处理结束后、调用resolve 或 reject
});
对通过
new
生成的promise
对象为了设置其值在resolve
(成功) /reject
(失败)时调用的回调函数 可以使用promise.then()
实例方法。onFulfilled、onRejected
两个都为可选参数。一般情况,onFulfilled
会用,onRejected
省略,一般用catch
比较多
promise.then(onFulfilled, onRejected);
promise.then
成功和失败时都可以使用。异常的时候调用promise.catch(onRejected)
像
Promise
这样的全局对象还拥有一些静态方法。包括Promise.all()
还有Promise.resolve()
等在内,主要都是一些对Promise
进行操作的辅助方法。用
new Promise
实例化的promise
对象有以下三个状态。其中 左侧为在ES6 Promises
规范中定义的术语, 而右侧则是在Promises/A+
中描述状态的术语。基本上状态在代码中是不会涉及到的,所以名称也无需太在意。
(1)"has-resolution" - Fulfilled: resolve(成功)
时。此时会调用onFulfilled
(2)"has-rejection" - Rejected: reject
(失败)时。此时会调用onRejected
(3)"unresolved" - Pending:
既不是resolve
也不是reject
的状态。也就是promise
对象刚被创建后的初始化状态等
-
promise
对象的状态,从Pending
转换为Fulfilled
或Rejected
之后, 这个promise
对象的状态就不会再发生任何变化。也就是说,Promise
与Event
等不同,在.then
后执行的函数可以肯定地说只会被调用一次。
基本流程
-
new Promise(fn)
返回一个promise
对象 - 在
fn
中指定异步等处理
处理结果正常的话,调用resolve(处理结果值);
处理结果错误的话,调用reject(Error对象);
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
// 运行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(function onRejected(error){
console.error(error);
});
- 本质上还是回调函数。将回调函数用
promise
对象包装一下。 -
resolve
方法的参数并没有特别的规则,基本上把要传给回调函数参数放进去就可以了。 (then
方法可以接收到这个参数值) - 传给
reject
的参数也没有什么特殊的限制,一般只要是Erro
r对象(或者继承自Error
对象)就可以。 - 其实
.catch
只是promise.then(undefined, onRejected)
的别名而已。一般说来,使用.catch
来将resolve
和reject
处理分开来写是比较推荐的做法。 -
Promise.resolve
作为new Promise()
的快捷方式,在进行promise
对象的初始化或者编写测试代码的时候都非常方便。
异步执行顺序
在使用Promise.resolve(value)
等方法的时候,如果promise
对象立刻就能进入resolve
状态的话,那么你是不是觉得.then
里面指定的方法就是同步调用的呢?
实际上,.then
中指定的方法调用是异步进行的。下面是一个例子:
var promise = new Promise(function (resolve){
console.log("inner promise"); // 1
resolve(42);
});
promise.then(function(value){
console.log(value); // 3
});
console.log("outer promise"); // 2
- 由于
JavaScript
代码会按照文件的从上到下的顺序执行,所以最开始<1>
会执行,然后是resolve(42);
被执行。这时候promise
对象的已经变为确定状态,FulFilled
被设置为了42
。 - 下面的代码
promise.then
注册了<3>
这个回调函数,但是这个是异步的,这里不会马上执行。 - 函数
<2>
会最先被调用,最后才会调用回调函数<3> 。
输出结果:
inner promise // 1
outer promise // 2
42 // 3
明明可以以同步方式进行调用的函数,非要使用异步的调用方式,这是在Promise
设计上的规定方针。
不要对异步回调函数进行同步调用 Promise
是符合这一点的。
方法链的形式
在Promise
里可以将任意个方法连在一起作为一个方法链(method chain)
。
aPromise.then(function taskA(value){
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){
console.log(error);
});
如果把在then
中注册的每个回调函数称为task
的话,那么我们就可以通过Promise
方法链方式来编写能以taskA → task B
这种流程进行处理的逻辑了。
- 每次调用
then
都会返回一个新创建的promise
对象。如果不用链式调用,那么就没有顺序执行的效果,要极力避免。
// 1: 对同一个promise对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
resolve(100);
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
console.log("1: " + value); // => 100
})
// 这种是过程式调用,没有顺序执行的功能,每个函数的参数value都是100,需要极力避免这种调用
// 2: 对 `then` 进行 promise chain 方式进行调用
var bPromise = new Promise(function (resolve) {
resolve(100);
});
bPromise.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log("2: " + value); // => 100 * 2 * 2 = 400
});
// 这种链式调用,才是期望的样子。
// 就算是简单的值(这里是100),在传递的时候,传的也是promise对象,中间有“装箱”,“拆箱”的操作。
// then函数,就是那种输入输出都是“类型”的函数,能够形成链式调用。所以说promise是monad的一种实现
- 在实际使用中,要避免下面的错误用法
function badAsyncCall() {
var promise = Promise.resolve();
promise.then(function() {
// 任意处理
return newVar;
});
return promise;
}
在 promise.then
中产生的异常不会被外部捕获,也不能得到then
的返回值,即使其有返回值。原因是promise !== promise.then()
- 应该该为下面的正确形式
function anAsyncCall() {
var promise = Promise.resolve();
return promise.then(function() {
// 任意处理
return newVar;
});
}
多个异步调用进行统一处理
- 为了应对这种需要对多个异步调用进行统一处理的场景,
Promise
准备了Promise.all
和Promise.race
这两个静态方法。 -
Promise.all
接收一个promise
对象的数组作为参数,当这个数组里的所有promise
对象全部变为resolve
或reject
状态的时候,它才会去调用.then
方法。 - 传递给
Promise.all
的promise
并不是一个个的顺序执行的,而是同时开始、并行执行的。
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
var startDate = Date.now();
// 所有promise变为resolve后程序退出
Promise.all([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then(function (values) {
console.log(Date.now() - startDate + 'ms');
// 约128ms
console.log(values); // [1,32,64,128]
});
// 输出:有调度损耗,线程间同步损耗,理论上应该输出128ms,实际输出145ms是合理的。多执行几次,这个值每次都会变
// 145ms
// [ 1, 32, 64, 128 ]
- 4个
promise
是并行执行的- 4个
promise
都执行完毕后再执行then
- 传递的
values
数组和promise
数组的顺序一致
-
Promise.race
只要有一个promise
对象进入FulFilled
或者Rejected
状态的话,就会继续进行后面的处理。
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
// 任何一个promise变为resolve或reject 的话程序就停止运行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then(function (value) {
console.log(value); // => 1
});
-
Promise.race
在第一个promise
对象变为Fulfilled
之后,并不会取消其他promise
对象的执行。
var winnerPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('winner funtion running');
resolve('this is winner');
}, 10);
});
var loserPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('loser funtion running');
resolve('this is loser');
}, 1000);
});
// 第一个promise变为resolve后程序停止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
console.log(value); // => 'this is winner'
});
// 输出:
// winner funtion running
// this is winner
// loser funtion running
- 数组中
[winnerPromise, loserPromise]
中的promise
同时开始执行10ms
后winnerPromise
胜出,输出winner funtion running
Promise.race()
函数有了结果,就是winnerPromise
,顺序执行then
,输出传递的value
,这里是this is winner
。这个就是winnerPromise
的resolve
信息resolve('this is winner');
loserPromise
对象没有被取消,继续执行。1000ms
后,loserPromise
对象中的函数执行,输出loser funtion running
*loserPromise
对象中的resolve
信息'resolve('this is loser');
不会被传递。
错误处理
-
.then
和.catch
都会创建并返回一个 新的promise
对象。Promise
实际上每次在方法链中增加一次处理的时候所操作的都不是完全相同的promise
对象。
使用
promise.then(onFulfilled, onRejected)
的话,在onFulfilled
中发生异常的话,在onRejected
中是捕获不到这个异常的。原因是他们在promise chain
中处于同一级在
promise.then(onFulfilled).catch(onRejected)
的情况下.then
中产生的异常能在.catch
中捕获。因为.catch
处于.then
中的下一级.then
和.catch
在本质上是没有区别的,所不同的是在整个promise chain
中所处的级别不同。.catch
更靠后一点,更可靠一点。如果一定要用
.then
来进行错误处理,那么就要新增一个下一级的.then
来处理
function throwError(value) {
// 抛出异常
throw new Error(value);
}
// <1> onRejected不会被调用
function badMain(onRejected) {
return Promise.resolve(42).then(throwError, onRejected);
}
// <2> 有异常发生时onRejected会被调用
function goodMain(onRejected) {
return Promise.resolve(42).then(throwError).catch(onRejected);
}
// <3> 使用下一级的then
function nextThenMain(onRejected) {
return Promise.resolve(42).then(throwError).then(null, onRejected);
}
// 运行示例
badMain(function(){
console.log("BAD");
});
goodMain(function(){
console.log("GOOD");
});
nextThenMain(function(){
console.log("Next Then");
});
// 输出:
// GOOD
// Next Then
这里的badMain
,之所以捕获不到错误,是因为用了同一级的.then
错误处理统一使用.catch
,不要用.then
- 主动抛异常,使用
reject
而不是throw
// 不要用throw,这虽然能运行,但很不promise风格,要避免
var promise = new Promise(function(resolve, reject){
throw new Error("message");
});
promise.catch(function(error){
console.error(error);// => "message"
});
// 这是推荐的方式,形成习惯
var promise = new Promise(function(resolve, reject){
reject(new Error("message"));
});
promise.catch(function(error){
console.error(error);// => "message"
})
- 在.then中使用reject,推荐用下面的格式
var onRejected = console.error.bind(console);
var promise = Promise.resolve();
promise.then(function () {
return Promise.reject(new Error("this promise is rejected"));
}).catch(onRejected);
// this promise is rejected