原创性声明:本文完全为笔者原创,请尊重笔者劳动力。转载务必注明原文地址。
发现有一段时间没有写文章了,中了一段时间农药的毒,后面都变得有点懒了。
今天写写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(成功)时。此时会调用 onFulfilled -
has-rejection-Rejected(译文:拒绝)
reject(失败)时。此时会调用 onRejected -
unresolved-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
上面的代码的执行流程用图来展示,就像这样:
image.png
显然,
catch会处理TaskA,也会处理TaskB。那如果TaskA发生了错误,还会执行TaskB吗? 答案是不会。如下图:
image.png
这时,我们再回到文章最开头的那个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对象)。
image.png
如上图:两个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方法,避免了繁杂的非空判断,提升了开发体验和代码的优雅性。
【参考】:













网友评论