promise是用于实现异步编程的一种解决方案,异步操作保存在promise中,在promise状态发生改变时触发对应的异步操作。
Promise的特点
- promise有三种状态,pending(进行中),resolve(已成功),rejected(已失败)。只有异步操作的结果才能改变promise的状态,其他操作无法改变其状态。
- Promise的状态转换只有两种,从pending转为resolve或者从pending转为rejected。一旦转换完成,状态就不再改变。如果状态改变已经发生了,再调用这个Promise对象,会立即返回结果。这与事件不同,事件一旦错过发生的时候,再监听该事件不会得到结果。
- Promise无法取消,一旦新建就会立即执行
- 其内部抛出的错误不会反应到外部
- 处于pending状态时,无法得知当前所处的状态,即离状态改变还有多“久”(刚开始进行还是即将完成)
Promise的基本用法
Promise使用Promise的构造方法来构建,该构造方法传入一个函数作为参数,传入的函数有两个参数,resolve和reject。resolve用于将状态由pending转为resolve,reject用于将pending转为rejected。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
resolve和reject是可以自己定义的命名,但是为了规范化代码,让别人在看你的代码时容易理解,建议使用resolve和reject。
resolve和reject中的参数将作为实例生成后then方法中的参数函数中的对应参数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then方法第一个参数为异步操作成功的回调函数,即resolve,第二个参数为异步操作失败的回调函数,即rejected。
then方法的回调函数在当前脚本所有同步任务执行完后才会执行,但会在setTimeout时间为0的语句之前执行,所以下面的代码的执行顺序与代码顺序不同。
setTimeout(function(){
console.log('setTimeout');
},0);
var pro = new Promise((resolve, reject) => {
resolve();
console.log('promise');
});
pro.then(function() {
console.log('resolved');
});
console.log('out');
//promise
//out
//resolved
//setTimeout
上面的代码可以看到,promise新建时立即执行了里面的console语句,接着将当前脚本中的同步任务即全局下的console.log(‘out’)执行后才执行then里面的console语句。
Promise构造器第一个参数回调会展开thenable或真正的Promise,与Promise.resolve(...)一样(下文会介绍该方法)。
var p=new Promise((resolve,reject)=>{
resolve(new Promise((resolve,reject)=>{
reject('error');
}))
})
p.then(null,(val)=>{
console.log(val);
})
//error
可以看到,resolve直接将里面的Promise展开,因为里面使用了reject,所以将里面的'error'作为then方法第二个函数参数的参数值。因此,当使用resolve方法时并不一定就是转为完成状态,如果其参数为一个Promise或thenable,在展开后可能会变为拒绝。
而reject并不像resolve一样,reject只会将传入的Promise/thenable值原封不动地设置为拒绝理由,不会展开Promise/thenable。
var p=new Promise((resolve,reject)=>{
reject(new Promise((resolve,reject)=>{
resolve('resolve');
}))
})
p.then(null,(val)=>{
console.log(val);
})
//Promise {<resolved>: "resolve"}
可以看到reject最后并没有展开里面的Promise,而是将其整个作为拒绝理由传给了then方法。
resolve和reject函数的参数是另一个promise时当前promise的状态由参数的promise决定。
var p1 = new Promise((resolve, reject) => {
resolve(p2);
});
var p2 = new Promise((resolve, reject) => {
resolve('p2');
})
p1.then((value) => {
console.log(value)
})
// p2
调用resolve和reject后接下来的语句仍会执行
var p1 = new Promise((resolve, reject) => {
resolve('resolve');
console.log('after');
});
p1.then((value) => {
console.log(value)
})
// after
// resolve
为了避免这种情况,我们将resoleve放在return后
var p1 = new Promise(
(resolve, reject) => {
return resolve('resolve');
console.log('after');
});
p1.then((value) => {
console.log(value)
})
// resolve
与setTimeout的执行顺序先后
对于propmise和setTimeout来说,两者都是异步的方式,但是还是有执行的先后顺序的
var r = new Promise(function(resolve, reject){
console.log("a");
resolve()
});
setTimeout(()=>console.log("d"), 0)
r.then(() => console.log("c"));
console.log("b")
上面代码的执行顺序是‘a,b,c,d’,c在d之前,是因为Promise产生的是JavaScript引擎内部的微任务,而setTimeout是浏览器API,它产生宏任务。在JavaScript执行机制中,异步任务的执行优先级是先执行微任务,后执行宏任务。(准确来说是先将微任务放入到执行栈执行,待微任务为空时再去宏任务中将任务放到执行栈中执行)
Promise的方法
Promise的原型方法
Promise.prototyep.then
then方法用于为Promise提供状态改变时添加对应的回调函数,其两个参数都为函数,第一个参数为Promise状态转为resolve时的回调函数,第二个参数为Promise状态转为rejected时的回调函数。该方法返回一个新的Promise实例(不是原来的Promise对象),即可以使用链式写法多次调用then方法。
getJSON("/posts.json")
.then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
多个then方法链式调用时,会等待上一个then方法返回后才执行下一个。
如果没有给then方法传递一个有效的函数作为完成处理函数参数,会有替代的默认函数参数。
var p=new Promise((resolve,reject)=>{
resolve(1);
})
p.then(
null
//默认变为下列的函数参数,将接收到的传入值传给下一个Promise
//function(val){
// return val;
//}
).then((val)=>{
console.log(val);
})
//1
我们可以看到,我们在then方法中传入了一个null,这并不是一个有效的函数参数,所以其被默认的函数参数替代,将之前的传入值“1”传入下一个Promise,最后通过console语句打印出来。同样的,then方法的第二个参数省略或者传入null,undefined会使用默认函数参数,将错误传递下去。
var p=new Promise((resolve,reject)=>{
reject('e');
});
p.then(null,null
//等同于
//function(val)=>{
// return(val);
//}
).then(null,(error)=>{
console.error(`error: ${error}`);
})
// error: e
Promise.prototype.catch
Promise.prototype.catch(reject)可以看成是Promise.prototype.then(null,reject)或Promise.prototype.then(undefined,reject),即该方法是Promise转换状态时发生错误的回调函数,因为null和undefined为被转化为将值传到返回的Promise的默认函数参数。
const promise = new Promise(function(resolve, reject) {
throw new Error('test');});
promise.catch(function(error) {
console.log(error);});
// Error: test
上面代码可以看到,在Promise中抛出了一个错误,catch方法捕获了这个错误。
若Promise已经进入了resolve状态,则抛出错误不会被catch方法捕获,这是因为Promise状态只要转换成resolve或rejected就不会再变换状态的特点导致的。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代码中,promise已经调用了resolve,所以其下面的抛出异常语句没有被catch方法捕获。
Promise对象抛出的错误如果没被捕获会传递到返回的Promise实例上,知道被捕获为止。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);})
.then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
上面代码中有三个Promise对象,不管哪一个抛出异常,最后都由catch来捕获。
如果没有指定处理错误的回调函数,Promise抛出的错误不会有任何反应,不会影响到外层代码的执行。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => {
console.log(123);
}, 2000);
// 123
如上面代码中虽然Promise报错了,但是外面的setTimeout方法依然可以执行。
不仅是Promise中会抛出错误,回调函数中也会抛出错误,报错catch中的函数,此时可以再用一个catch来处理抛出的错误。
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错,因为y没有声明
y + 2;})
.catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]
Promise.prototype.finally
该方法用于执行Promise不管转换成什么状态都会执行的操作,可以类比try/catch操作中的finally。
Promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
如上代码中,不管promise最后变为什么状态,finally后的参数函数一定会执行。
finally方法的回调函数不接受参数,这意味着无法得知当前是什么状态,所以finally用来执行与状态无关的操作,其本质为then方法的特例。其实现如下代码。
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
finally方法最后会返回resolve或reject的参数值。
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
转为Promise对象的方法
Promise.resolve
该方法用于将参数转为Promise对象
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
由上面的等价写法我们可以看出Promise.resolve方法返回一个转换为resolve状态的Promise对象,根据其参数有四种不同的返回情况
1.参数为一个Promise实例
此时该方法会直接返回参数中的Promise实例
var p=new Promise((resolve,reject)=>{
resolve();
})
Promise.resolve(p)===p
//true
2.参数是一个有then方法的对象
var thenObj = {
then(resolve) {
console.log('then');
resolve('resolve');
}
}
Promise.resolve(thenObj).then(value => {
console.log(value);
})
// then
// resolve
在使用该方法将对象转为Promise对象时会立即执行该对象的then方法,所以上面代码中首先打印出’then’,然后才执行resolve的回调函数。
3.参数为一个没有then方法的对象或非对象
此时该方法返回resolve的参数为该方法参数的Promise对象
const p = Promise.resolve('Hello');
p.then(function (val){
console.log(val)
});
// Hello
4.没有参数
此时该方法直接返回一个Promise对象,由于没有参数,所以此时打印出resolve的参数只能得到undefined
var p = Promise.resolve();
p.then(function (val) {
console.log(val);
});
// undefined
Promise.reject
该方法也接受一个参数,返回一个Promise对象,与Promise.resolve不同的是,该方法返回的Promise对象的状态为rejected。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)});
// 出错了
对于不同参数的处理,Promise.reject与Promise.resolve一样,Promise.reject参数为Promise或thenable时,会将整个Promise/thenable作为拒绝理由传给返回的Promise。
var p1=new Promise((resolve)=>{
resolve(1);
})
var p2=Promise.reject(p1);
p2.then(null,(val)=>{
console.log(val);
})
// Promise {<resolved>: 1}
Promise处理多个Promise的方法
Promise.all
该方法用于将多个Promise实例包装为一个新的Promise实例。
该方法接受一个数组作为参数,数组成员为Promise对象,若不是Promise对象,则先使用Promise.resolve方法变为对象。而事实上,数组参数中的每个值都会经过Promise.resolve方法的过滤。
该方法返回的Promise对象的状态由参数数组中的Promise对象决定,若数组中所有Promise对象的状态都变成已成功,则该方法返回的Promise对象的状态也变为已成功,数组中所有Promise的返回值组成一个数组传递给该方法生成的Promise对象的回调函数;若数组中有一个Promise对象状态变为已失败,则第一个变为已失败的对象的返回值传递给最后返回的Promise对象,该对象状态也变为已失败。
Promise.all([1,2,3,4]).then((val)=>{
console.log(val);
})
// [1, 2, 3, 4]
Promise.all([p1,p2,p3,p4]).then((val)=>{
console.log(val);
}).catch((error)=>{
console.log(`error: ${error}`);
})
// error: 4
由上面的代码可以看到,当参数数组里面没有一个状态变为失败,会将成功的返回值作为数组成员并返回该数组,所以第一段代码返回了一个数组。而第二段代码中,因为p4是变为失败的,所以最后只将失败的值传给返回的Promise对象。
Promise.race
该方法的参数和Promise.resolve一样,不同的是,其状态由参数数组成员中率先改变的Promise对象决定,返回的Promise对象会与率先改变的Promise对象相同,率先改变的Promise对象的返回值也会传递给该方法返回的Promise对象。
var p1=new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},1000);
})
var p2=new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},2000);
})
var p3=new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000);
})
Promise.race([p1,p2,p3]).then((val)=>{
console.log(val);
})
// 1
如上代码中,p1转为成功最快,所以最后将p1中的resolve的参数作为最后返回的值。
如果参数为一个空数组,按我们上文说的,最后得到的Promise的状态是由其内率先转换的Promise决定的,因为空数组内显然没有Promise,自然更没有率先转换的Promise,所以返回Promise永远不会转换状态,也因此千万不能使用空数组作为其参数。如下面的代码就永远不会转换状态。
Promise.race([]).then((val)=>{
console.log(val);
}).catch((error)=>{
console.error(error);
});
如果参数数组中的成员不是Promise对象,一样会先使用Promise.resolve将其变为对象。
其他方法
Promise.try
该方法用于模拟try代码块,通过该方法可以使同步函数同步执行,异步函数异步执行。
如下面代码中,database.users.get可能会抛出同步错误也可能抛出异步错误,对应不同的错误写法如下。
//异步错误
database.users.get({id: userId})
.then(...)
.catch(...)
//同步错误
try {
database.users.get({id: userId})
.then(...)
.catch(...)} catch (e) {
// ...}
而通过使用Promise.try可以同时处理这两种错误
Promise.try(() => database.users.get({id: userId}))
.then(...)
.catch(...)
Promise的局限性
顺序错误处理
Promise的错误处理可以使用then方法,catch方法来处理,在对一条Promise链进行错误处理时,可能出现每个步骤都出现错误,且在每个步骤都有错误处理的方法的情况,此时我们在最后使用catch只能捕获最后的错误,而无法得到中间的错误,即中间的错误会被忽略。这有时是我们要的,但有时候又可能是我们想避免而没法避免的。
var p=new Promise((resolve,reject)=>{
reject('e');
});
p.then(null,(val)=>{
foo.v();// foo为未定义的变量名,所以在这里会报错
console.log(val);
}).then(null,(val)=>{
console.log(val);
})
//ReferenceError: foo is not defined at p.then
上面代码中,第一个错误被忽略了,这就是Promise的一个局限性。
单一值
Promise只能有一个完成值(resolve)或一个拒绝理由(reject),一般我们会通过将多个值封装到一个数组或对象来实现多个值的传递。
单决议
Promise只能被决议一次,即只能转换一次状态(变为完成状态或者拒绝状态),改变之后没法变为另一种状态,也无法变为原状态。这是Promise的本质特征,也适用于很多场景,但当我们要用于响应某种可能发生多次的激励(比如事件)时,Promise就不是很合适了,就如按钮的点击事件,除非你的按钮只能点一次,否则Promise不适合用在这种场景中。
无法取消
一旦创建了一个Promise并为其注册了完成或拒绝处理函数,且该任务因某种情况没法转换状态时,我们无法从外部停止其进程。就如给Promise.race传了一个空数组作为参数,Promise.race一直在等待某个Promise转为完成或拒绝状态,但我们都明白它永远等不到,但它也不会消失,我们也无法从外部取消它的等待。
Promise性能
使用Promise链比起使用基于回调的异步任务链进行的动作会多一些,这意味着它会稍微慢一些,但我们很难回答会慢多少。虽然这是局限性,但有时为了得到其他东西,我们可以接受这样的局限性。
Promise稍慢一些,但是作为交换,你得到的是大量内建的可信任性,对Zalgo的避免以及可组合性。
---------《你不知道的JavaScript 中卷》
参考自阮一峰的《ECMAScript6入门》
Kyle Simpson的《你不知道的JavaScript 中卷》
ES6学习笔记目录(持续更新中)