原创性声明:本文完全为笔者原创,请尊重笔者劳动力。转载务必注明原文地址。
发现有一段时间没有写文章了,中了一段时间农药的毒,后面都变得有点懒了。
今天写写javascript
中的这个Promise
,还记得第一次碰到这个东西是好久以前在angularJS
的环境下。angularJS
的$resource
获取了后端RESTfull api
接口的数据后是一个Promise
对象,直接用双向绑定到DOM上是能正确显示的,但是控制台打印它里面的某个属性值死活就是undefined
,后来磕磕碰碰摸索久了就知道有个then
,今天再来总结一下这个东西。
前戏
Promise
译为保证。表示异步处理的方法承诺返回一个值。Promise
对象也是用于解决异步中晦涩复杂的回调现象。至于如何解决,不妨先看看在Promise
之前,一个异步回调的例子:
var getData = function(url, callback) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
callback(req);
};
req.send();
}
var url = 'api/user/username/:id';
getData(url, function(req) {
if (req.status == 200) {
$('#username').text(req.statusText); // 用jquery将请求的数据写入dom
} else {
console.error(new Error(req.statusText);
}
});
如果拿到username
后不是显示这么简单,还需要把username
作为请求参数继续请求别的数据(例如:age),然后展示age
呢。
实际情况当然是直接请求一个user就行了,这里只是举例方便。
改动一下代码:
var getData = function(url, callback) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
callback(req.responseText);
};
req.send();
}
var url = 'api/user/username/:id';
getData(url, function(data) {
});
var url = 'api/user/username/:id';
getData(url, function(req) {
if (req.status == 200) {
var url = 'api/user/age/:username';
getData(url, function(req) {
if (req.status == 200) {
$('#age').text(req.statusText); // 用jquery将请求的数据写入dom
} else {
console.error(new Error(req.statusText));
}
});
} else {
console.error(new Error(req.statusText));
}
});
显然,这看起来不仅有点理解费劲,而且代码也不好看。但是,要用回调实现这里面的异步请求,只能这样干。事实上,这是一种强行异步变同步的办法,通过回调,以确保数据拿到之后再做下一步的处理,所以这是事实上的同步。如果用Promise
,会怎么样呢?
var getData = function(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.send();
});
}
var url = 'api/user/username/:id';
getData(url).then(function(username) {
var url = 'api/user/age/:username';
getData(url).then(function(age) {
$('#age').text(age); // 用jquery将请求的数据写入dom
}, function(error) {
console.log(error);
});
}, function(error) {
console.log(error);
})
乍一看,代码也没有少很多嘛。的确,但是仔细理解这两段代码时,就会感受到它们的区别。采用Promise
时,调用getData
时你不需要关心它内部的代码,给你的感觉就是getData(url)
已经返回了我想要的结果(username
),于是自然地就在then()
方法中的第一个参数(匿名函数)中直接用这个结果(username
)去发起下一个请求(请求age
),而在then()
的第二个参数(匿名函数)中对错误进行处理。
而采用回调方法,你总是需要去把该参数函数带入getData
中。虽然它代码的顺序看上去和Promise
很像,但代码执行的流程却是离不开回调的本质。如果回调多了,就可能会陷入回调地狱。理解和维护都会很蛋疼。
Promise
上面的例子就基本阐明了Promise
的简单用法。构建一个Promise
对象可以这样:
var promise = new Promise(function(resolve, reject) {
// 异步处理,处理结束后、调用resolve 或 reject
});
然后就可以调用Promise
实例的then
方法:
promise.then(onFulfilled, onRejected);
onFulfilled
, onRejected
是两个参数函数。
-
resolve
(成功)时:
onFulfilled
会被调用 -
reject
(失败)时
onRejected
会被调用
也就是说,promise
执行成功(一般是后端交互请求数据),那么就会调用onFulfilled
方法,所以在这个方法里继续写拿到数据后要执行的代码即可,如果发生了错误(例如:后端报错),则会执行onRejected
方法,在里面定义错误处理的代码即可。
上面涉及
Promise
的两个API
方法:1. 构造函数Promise()
; 2.实例方法then()
。
怎么把一个常量包装为Promise
对象?
var promise = Promise.resolve(99);
当然,这个对象的then
方法就完全没有第二个函数参数的必要了。
说到第二个函数参数onRejected
,它是用于发生异常时被调用的。此外,还可以通过catch
来捕获异常,如下:
var promise = Promise.resolve(99);
promise.then(function(value){
console.log(value); // 99
}).catch(function(error){
console.log(error);
})
与onRejected
效果一致。当然,此处的catch
也没有意义。
Promise.resolve(99)
可以认为是下面代码片段的语法糖:
Promise.resolve(99).then(function(value){
resolve(value); // 此时,这个promise对象立即进入确定(即resolved)状态,并将99传递给后面then里所指定的 onFulfilled 函数
});
这里涉及了
Promise
的一个静态方法:resolve
,除构造方法之外的另一个构建Promise
的方法。
Promise的状态
用new Promise 实例化的promise对象有以下三个状态。
has-resolution
-Fulfilled
(译文:履行)
resolve(成功)时。此时会调用 onFulfilledhas-rejection
-Rejected
(译文:拒绝)
reject(失败)时。此时会调用 onRejectedunresolved
-Pending
(译文:未决定)
既不是resolve
也不是reject
的状态。也就是promise对象刚被创建后的初始化状态等。
promise
对象的状态,从Pending
转换为Fulfilled
或Rejected
之后, 这个promise
对象的状态就不会再发生任何变化。也就是说,Promise
与Event
等不同,在.then
后执行的函数可以肯定地说只会被调用一次。另外,Fulfilled
和Rejected
这两个中的任一状态都可以表示为Settled
(不变的,稳妥的:表示这个Promise的状态已经回归稳定,即完成了Fulfilled
或Rejected
)。
Thenable对象
什么是Thenable
对象?从名字上看,就是可以then
的对象——可以调用then
方法的对象。除了Promise
还有对象可以调用then
方法吗?答案是肯定的,比如:
$.ajax('/json/comment.json'); // 可以继续`.then()`
这是jquery的jqXHR
对象,它其实就是一个Thenable
对象。但它不是一个Promise
,因为不符合ECMAScript 6
中的标准。
如何将它转换为一个标准的Promise
对象呢?
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise对象
promise.then(function(value){
console.log(value);
});
- 简单总结一下 Promise.resolve 方法的话,可以认为它的作用就是将传递给它的参数填充(Fulfilled)到promise对象后并返回这个promise对象。
- 此外,Promise的很多处理内部也是使用了 Promise.resolve 算法将值转换为promise对象后再进行处理的。
Promise方法链(promise chain
)
前面,接触了then().catch()
这种链式调用的方式,其实Promise
的链式调用更加丰富:
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
}
function onRejected(error) {
console.log("Catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
结果显而易见:
Task A
Task B
Final Task
上面的代码的执行流程用图来展示,就像这样:
显然,
catch
会处理
TaskA
,也会处理
TaskB
。那如果
TaskA
发生了错误,还会执行
TaskB
吗? 答案是不会。如下图:
这时,我们再回到文章最开头的那个Promise
例子,先根据id
获取username
,再根据username
获取age
,最后写入dom中,这个过程显然对于promise chain
链式调用的应用场景再符合不过了:
var getData = function(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.send();
});
}
var url = 'api/user/username/:id';
getData(url)
.then(function(username) {
var url = 'api/user/age/:username';
return getData(url); // a. 向下一个then中传递参数
}).then(function(age) {
$('#age').text(age);
}).catch(function(err){
console.log(err);
});
我们是怎样往第二个
then
传递异步查询的结果age
呢?通过一个关键的return
!
通过连续的then
方法调用,整个流程(两个步骤)无论在理解上,还是代码美观上,都变得非常优雅!甚至让你在编写它的时候忘记它原来是个异步的过程——这简直和写同步代码是一样一样的嘛。
另外,我们还注意到,原本应该有两个catch
变成了一个catch
,倒金字塔
的恶心没有了,满屏的花括号也没有了。流程变得清晰。这就是使用Promise
的良好开发体验。
【疑问】:稍微细心点,不难发现
(a)
处return
的是getData(url)
,这个结果应该是一个Promise
对象,而不是一个普通值(例如: 24(岁)),为什么这样也可以在下一个then
中通过age
参数接收到呢?经过实践测试,返回普通值
或Promise
对象,都可以正常接收。
【结论】:每个方法中 return 的值不仅只局限于字符串或者数值类型,也可以是对象或者promise对象等复杂类型。
事实上,在每次调用then
方法后,都会返回一个新的Promise
对象,以供下一次链式调用。如果then
中匿名函数返回的是普通类型,Promise
在内部也会将其转换为一个新的Promise
对象(注意:是新的Promise
对象)。
如上图:两个promise object
一定是两个不同的Promise
对象,尽管then
中可能什么也没有做,他们resolve之后的值也尽管可能相同。
由此,可能引出一个可能常犯的错误:
不正确的:
function asyncCall() {
var promise = Promise.resolve();
promise.then(function() { // a
// 任意处理
return newVar;
});
return promise;
}
(a)
处调用then
时,返回的是一个新的Promise
对象,而不再是promise
那个变量,因此这个then
中的异常是不会被外部捕获的,而且它的返回值也无法在外部得到。
正确的:
function asyncCall() {
var promise = Promise.resolve();
return promise.then(function() {
// 任意处理
return newVar;
});
}
或者
function asyncCall() {
var promise = Promise.resolve();
var promise1 = promise.then(function() {
// 任意处理
return newVar;
});
return promise1;
}
Promise.all
方法
这个方法之前没有提到过。仍以最开始案例延伸。现在我们不需要根据username
获取age
了,可以直接根据id
获取age
,也就是说我们有两个请求:1. 根据id获取username
,2. 根据id获取age
。我们希望这两个请求都完成后,再将结果username
和age
插入到DOM中。这时,Promise.all
就可以用上了:
var getData = function(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.send();
});
}
var urlUsername = 'api/user/username/:id';
var urlAge = 'api/user/age/:id';
var requestArr = [getData(urlUsername), getData(urlAge)]; // 两个Promise对象组成的数组
Promise.all(requestArr).then(function(data) { // a. data的值是什么呢?
var username = data[0]; // b
var age = data[1]; // c
$('#username').text(username);
$('#age').text(age);
}).catch(function(err) {
console.log(err);
});
可以看出,Promise.all
的参数是一个数组,这个数组的成员是Promise
对象。有点像把多个异步请求放到一个Promise
里处理的感觉,这样做的好处是一来代码简洁了一些,更重要的是(a)
处的then
方法能确保在两个请求完成后再执行,也就是两个Promise
对象的状态都变成了Settled
。
Promise.all 在接收到的所有的对象promise都变为 FulFilled 或者 Rejected 状态之后才会继续进行后面的处理
。
那(a)
处的data
又是什么值呢?username
,or age
?
显然从(b)
和(c)
的代码可以看到data
是个数组,对应于requestArr
中两个Promise
对象所代表的值(顺序也是保持一致的)。
用
Promise.all
处理的多个Promise
不是按顺序执行的,而是同时、并行执行。也就是说上面的两个请求是同时、并发进行的,而不是请求了username
,再请求age
。
Promise.race方法
理解了Promise.all
,再看Promise.race
就简单了。Promise.race
的用法和all
一样,也是接收一个Promise
对象组成的数组,resolve
后的值(data
)格式也是一样的。不同之处在于:
Promise.race只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理
。
同样以上面那个例子。如果将all
改为race
,那么只要有一个promise
先Settled
,then
就会开始执行,而其他promise
就不会继续执行then
方法了。同样then
中接收到的data,也不再是数组,而是先Settled
的那个Promise
对象resolve
的值。
从race
这个意思,也能大致看出它的这个特性。
Deferred和Promise
Deferred
译为延期。它和Promise
不同,它没有共通的规范,换句话说,在ECMAScript
内容里有对Promise
的定义标准,但是没有Deferred
的定义标准,更通俗的说,你用javascript
语言可以直接像用Math
、Date
那样直接在代码里去用Promise
,但是Deferred
是不存在的。它的存在纯粹是市面上各种类库实现出来的。我们自己也可以简单实现一个Deferred
类:
deferred.js
function Deferred() {
this.promise = new Promise(function (resolve, reject) {
this._resolve = resolve;
this._reject = reject;
}.bind(this));
}
Deferred.prototype.resolve = function (value) {
this._resolve.call(this.promise, value);
};
Deferred.prototype.reject = function (reason) {
this._reject.call(this.promise, reason);
};
上面的 Function Deferred
就是我们实现的Deferred
类。熟悉bind
、call
和原型继承
的话,不难理解上面代码的含义:新建了一个对象Deferred,它有一个属性promise, 内部构建了一个Promise实例,把这个实例的一些api方法通过原型继承给了Deferred,Deferred可以通过调用自己的resolve或reject等方法控制其属性promise的状态
。
为什么要这么麻烦呢?
为了更好的操作
Promise
。
比如,在之前的案例中,Promise
什么时候resolve
或reject
,我们通常只能在Promise
构造函数中调用。现在用上刚写的Deferred
,会显得更灵活,我们改造一下getData
:
var getData = function(url) {
var deferred = new Deferred();
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status == 200) {
deferred.resolve(req.responseText);
} else {
deferred.reject(new Error(req.statusText));
}
};
req.send();
return deferred.promise;
}
var url = 'api/user/username/:id';
getData(url).then(function(data) {
$('#username').text(data);
}).catch(function(err) {
console.log(err);
});
我们看到getData
里面已经看不到Promise
的构造函数了,用defered可以自由调用resolve
和reject
。试想,我们有一个Promise
对象,我们不知道它的状态是否已经Settled
,我们可以用Deferred
手动resolve
一下:
var promise = ... ;// 一个来源于其他地方的promise
var deferred = new Deferred();
deferred.resolve(promise);
return deferred.promise;
返回的deferred.promise
状态是确定的。
所以,其实Deferred
并不抽象,若要说抽象,那就是对call
、bind
和javascript 原型继承
的理解还不透彻。很多类库都有实现Deferred
这个东西,只是大家都约定俗成用Deferred
这个名字,例如jquery
、angular
,而且各类库的实现方式未必相同。
Deferred
和Promise
并不是处于竞争的关系,而是Deferred内涵了Promise,也就包装了一下。
【个人观点】:就像Promise
也是包装了一下resovle
之后的具体值,附加一些api方法,避开了繁杂的回调,提升了开发体验和阅读代码体验。这让我想起了java 8
中的Optional
,它也是对一个对象的包装,提供一些api方法,避免了繁杂的非空判断,提升了开发体验和代码的优雅性。
【参考】:
- JavaScript Promise迷你书;
- ECMAScript 6 入门