ES6 Promise对象之实例方法(2)
上一篇关于Promise的文章介绍了Promise的基本用法,这一篇继续上一篇,介绍Promise的各种方法:
(1)Promise.prototype.then()
then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前一篇关于Promise对象的文章中提到过then方法的两个参数,在此不再叙述。
then方法返回的是一个新的Promise实例(不是原来的Promise实例)。由此可以采用链式写法,即then方法后面再调用另一个then方法:
let promise = new Promise(function (resolve, reject) {
//some code
});
promise.then(function (value) {
//some code
}).then(function (value) {
//some code
});
使用then方法依次指定了两个回调函数,第一个回调函数完成以后,会将返回结果作为参数传入第二个回调函数。如果前一个then回调函数返回的还是一个Promise对象(即有异步操作),这时后一个then回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
(2)Promise.prototype.catch()
1.catch方法用于指定发生错误时的回调函数。
let promise = new Promise(function (resolve, reject) {
//some code
});
promise.then(function (value) {
//some code
}).catch(function (value) {
//some code
});
如果Promise实例对象状态变为Resolved,则调用then方法指定的回调函数;
如果异步操作抛出错误,Promise实例对象状态就会变为Rejected,就会调用catch方法指定的回调函数处理这个错误。
另外,then方法指定的回调函数如果在运行中抛出错误,也会被catch方法捕获。
2.catch方法的作用相当于then方法的第二个回调函数:
promise.then((value) => console.log(value))
.catch((error) => console.log(error));
// 等同于
promise.then((value) => console.log(value))
.then(null, (error) => console.log(error));
3.reject方法的作用等同于抛出错误
Promise抛出一个错误被catch方法指定的函数捕获,共有三种写法(三种写法是等价的):
let promise = new Promise((resolve, reject) => {
throw new Error("Error!")
});
promise.catch(function (error) {
console.log(error)
});
//Error: Error!
let promise = new Promise((resolve, reject) => {
try {
throw new Error("Error!")
}
catch (error) {
reject(error)
}
});
promise.catch(function (error) {
console.log(error)
});
//Error: Error!
let promise = new Promise((resolve, reject) => {
reject(new Error("Error!"))
});
promise.catch(function (error) {
console.log(error)
});
//Error: Error!
4.在resolve函数后面再抛出错误,并不会被捕获
let promise = new Promise((resolve, reject) => {
reject("Rejected!");
throw new Error("Error!")
});
promise.catch(function (error) {
console.log(error)
});
//Rejected!
5.Promise对象的错误会一直向后传递,直到被捕获为止
也就是说,错误总会被下一个catch语句捕获。
let promise = new Promise(function (resolve, reject) {
//some code
});
promise.then(function (value) {
//some code
}).then(function (value) {
//some code
}).catch(function (value) {
//some code
});
这里的catch方法会处理前面Promise实例对象以及两个then方法执行中抛出的错误。
一般来说,不要在then方法中定义Rejected状态的回调函数(then的第二个参数),最好是用catch方法:
因为使用catch方法可以捕获前面所有then方法中产生的错误,而采用then方法的第二个回调函数无法对同一个then方法中的第一个回调函数中的错误进行处理(对于前一个then方法中的错误可以进行处理)。
promise
.then(function(data){
//some code
}
.then(function(data){
//some code
},function(error){
//some code
});
promise
.then(function(data){
//some code
}
.then(function(data){
//some code
})
.catch(function(error){
//some code
});
第二种写法要好于第一种写法,因为第二种写法catch方法可以捕获前面所有then方法执行中的错误。
6.如果没有用catch方法,抛出的错误不会传递到外层代码
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);
在chrome中会打印如下:
// Uncaught (in promise) ReferenceError: x is not defined
// 123
而在火狐中打印如下(没有打印错误提示):
//123
上面代码中,someAsyncThing函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
再来看下面的例子:
const promise = new Promise(function (resolve, reject) {
resolve('ok');
setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
setTimeout(() => { console.log(123) }, 2000);
// ok
// Uncaught Error: test
// 123
上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。
一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。
7.catch方法之后还可以调用then方法
catch运行完后返回的还是一个Promise对象,因此后面还可以调用then方法。
Promise.resolve()
.catch(function (error) {
console.log("oh no!" + error);
})
.then(function (data) {
console.log("carry on!")
});
//carry on!
代码运行完catch方法的回调函数之后会接着运行后面那个then方法。
如果没有报错,就会跳过catch方法。
以上代码如果then中的回调函数执行中出现错误,就与前面的catch无关了。
8.catch方法还可以再抛出错误
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错,因为 y 没有声明
y + 2;
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
上面代码中,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]
上面代码中,第二个catch方法用来捕获前一个catch方法抛出的错误。
(3)Promise.resolve()
Promise.resolve用于将现有对象转为Promise对象。下面两种写法等价:
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
1.参数是一个Promise实例
如果参数时Promise实例,那么Promise.resolve将不做任何修改返回这个实例。
2.参数是一个thenable实例
thenable对象指的是具有then方法的对象,例如:
let thenable = {
then: function (resolve,reject) {
resolve(42);
}
}
将这个对象转为Promise对象:
let thenable = {
then: function (resolve,reject) {
resolve(42);
}
};
let p1=Promise.resolve(thenable);
p1.then(function(value){
console.log(value);
})
//42
3.参数不具有then方法的对象或根本不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,Promise.resolve方法会返回一个新的Promise对象,状态为Resolved。
下面我们先来看一下什么是原始值:
在ECMAScript中,变量可以存放两种类型的值:
a、原始值:固定而简单的值,是存放在栈(stack)中的简单数据段,它们的值直接存储在变量访问的位置。原始类型有以下五种类型:Undefined,Null,Boolean,Number,String;
b、引用值:存放在堆(heap)中的对象,存储在变量处的是一个指针,指向存储对象的内存地址。所有引用类型都集成自Object。
//字符串
let p1=Promise.resolve("hello");
p1.then(function(value){
console.log(value);
})
//hello
//数组
let p2=Promise.resolve([1,2,3]);
p2.then(function(value){
console.log(value);
})
//[1,2,3]
//不具有then方法的对象
let p3=Promise.resolve({"1":1});
p3.then(function(value){
console.log(value);
})
//{"1":1}
//null
let p4=Promise.resolve(null);
p4.then(function(value){
console.log(value);
})
//null
//undefined
let p5 = Promise.resolve(undefined);
p5.then(function (value) {
console.log(value);
})
//undefined
//数字
let p6 = Promise.resolve(123);
p6.then(function (value) {
console.log(value);
})
//123
//布尔值
let p7 = Promise.resolve(true);
p7.then(function (value) {
console.log(value);
})
//true
以上Promise实例返回的状态都是Resolved,所以会调用then方法的第一个回调函数。
4.不带有任何参数
直接返回一个Resolved状态的Promise对象。因此,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。
Promise.resolve()得到一个Promise对象变成Resolved状态是在本轮“事件循环”结束时,而不是在下一轮“事件循环”开始时。
setTimeout(function(){
console.log("three");
},0);
Promise.resolve().then(function(){
console.log("two");
});
console.log("one")
//one
//two
//three
上面代码中,setTimeout()是在下一轮“事件循环”开始的时候执行的,Promise.resolve()在本轮“事件循环”结束的时候执行,console.log(“one”)则立即执行。
(4)Promise.reject()
Promise.reject方法返回一个新的Promise实例,状态为Rejected:
下面两种写法等价:
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
Promise的实例对象p生成后状态为Rejected,回调函数会立即执行:
const p = Promise.reject('出错了');
p.then(null, function (s) {
console.log(s)
});
// 出错了
1.参数是一个Promise实例
将Promise实例的状态变为Rejected,并返回一个新的Promise对象。reject函数的参数是Promise.reject()方法中传入的参数(在这里是Promises实例a):
let a = Promise.resolve();
const p = Promise.reject(a);
p.then(null, function (s) {
console.log(s);// Promise对象
console.log(s===a);//true
});
console.log(p===a);//false 说明返回新的Promise对象
2.参数是一个thenable实例
将thenable对象转为Promise对象,新生成的Promise实例的状态为Rejected。reject函数的参数是Promise.reject()方法中传入的参数(在这里是对象thenable1):
let thenable1 = {
then: function (resolve,reject) {
reject(42);
}
};
let p1=Promise.reject(thenable1);
p1.then(null,function(error){
console.log(error);//Object
console.log(error===thenable1);//true
})
注意这里与Promise.resolve方法的不同:
//Promise.resolve方法
let thenable = {
then: function (resolve,reject) {
resolve(42);
}
};
let p1=Promise.resolve(thenable);
p1.then(function(value){
console.log(value);
});
//42
//Promise.reject方法
let thenable1 = {
then: function (resolve,reject) {
reject(42);
}
};
let p2=Promise.reject(thenable1);
p2.then(null,function(error){
console.log(error);//Object
console.log(error===thenable1);//true
})
reject函数的参数是Promise.reject()方法中传入的参数(这里是对象thenable1)。
3.参数不具有then方法的对象或根本不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,Promise.reject方法会返回一个新的Promise对象,状态为Rejected。
//字符串
let p1=Promise.reject("hello");
p1.then(null,function(value){
console.log(value);
});
//hello
//数组
let p2=Promise.reject([1,2,3]);
p2.then(null,function(value){
console.log(value);
})
//[1,2,3]
//不具有then方法的对象
let p3=Promise.reject({"1":1});
p3.then(null,function(value){
console.log(value);
})
//{"1":1}
//null
let p4=Promise.reject(null);
p4.then(null,function(value){
console.log(value);
})
//null
//undefined
let p5 = Promise.reject(undefined);
p5.then(null,function (value) {
console.log(value);
})
//undefined
//数字
let p6 = Promise.reject(123);
p6.then(null,function (value) {
console.log(value);
})
//123
//布尔值
let p7 = Promise.reject(true);
p7.then(null,function (value) {
console.log(value);
})
//true
以上Promise实例返回的状态都是Rejected,所以会调用then方法的第二个回调函数。
4.不带有任何参数
直接返回一个Resjected状态的Promise对象:
let p1=Promise.reject();
p1.then(null,function(error){
console.log(error);
})
//undefined
Promise.reject()得到一个Promise对象变成Rejected状态是在本轮“事件循环”结束时,而不是在下一轮“事件循环”开始时:
setTimeout(function(){
console.log("two");
},0);
let p1=Promise.reject();
p1.then(null,function(error){
console.log(error);
});
console.log("one");
//one
//undefined
//two
上面代码中,setTimeout()是在下一轮“事件循环”开始的时候执行的,Promise.reject()在本轮“事件循环”结束的时候执行,console.log(“one”)则立即执行。
(5)Promise.all()
Promise.all方法用于将多个Promise实例包装成一个新的Promise实例。
promise.all方法接受的参数要具有Iterator接口,并且返回的每个成员都是Promise实例,以数组为例:
let p=Promise.all([p1,p2,p3])
p1,p2,p3都是Promise实例;如果不是,就会先调用Promise.resolve方法将参数转为Promise实例,再进一步处理。
p的状态由p1、p2、p3决定,分成两种情况:
1、只有p1、p2、p3的状态都变成Fulfilled,p的状态才会变成Fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数:
let p1=Promise.resolve("hello");
p1.then(function(value){
console.log(value);
});
//hello
let p2=Promise.resolve([1,2,3]);
p2.then(function(value){
console.log(value);
});
//[1,2,3]
let p6 = Promise.resolve(123);
p6.then(function (value) {
console.log(value);
});
//123
Promise.all([p1, p2, p6]).then(function (posts) {
console.log(posts);
}).catch(function (reason) {
console.log(reason);
});
//["hello",[1,2,3],123]
2、只要p1、p2、p3之中有一个状态变成Rejected,p的状态就变成Rejected,此时第一个状态为Rejected的实例的返回值,会传递给p的回调函数:
let p1 = Promise.reject("hello");
p1.then(null, function (value) {
console.log(value);
});
//hello
let p2 = Promise.reject([1, 2, 3]);
p2.then(null, function (value) {
console.log(value);
});
//[1,2,3]
let p6 = Promise.reject(123);
p6.then(null, function (value) {
console.log(value);
});
//123
Promise.all([p1, p2, p6]).then(function (posts) {
console.log(posts);
}).catch(function (reason) {
console.log(reason);
});
//hello
如果作为参数的Promise实例自身定义了catch方法,那么它状态变为Rejected时并不会触发Promise.all()的catch方法;
let promise1 = new Promise((resolve, reject) => {
resolve();
}).then(result => console.log("promise1_then"))
.catch(e => console.log("promise1_catch"));
let promise2 = new Promise((resolve, reject) => {
throw new Error()
}).then(result => console.log("promise2_then"))
.catch(e => console.log("promise2_catch"));
Promise.all([promise1, promise2])
.then(result => console.log("promise_then"))
.catch(e => console.log("promise_catch"));
//promise1_then
//promise2_catch
//promise_then
在上面的例子中,promise1状态变为Resolved,promise2首先状态变为Rejected,之后执行catch中的回调函数,执行完成后,promise2指向一个新的Promise实例,这个实例的状态为Resolved,所以Promise.all()里的两个实例的状态都是Resolved,因此调用then方法中的回调函数。
修改一下:
let promise1 = new Promise((resolve, reject) => {
resolve();
}).then(result => console.log("promise1_then"))
.catch(e => console.log("promise1_catch"));
let promise2 = new Promise((resolve, reject) => {
throw new Error()
}).then(result => console.log("promise2_then"));
// .catch(e => console.log("promise2_catch"));
Promise.all([promise1, promise2])
.then(result => console.log("promise_then"))
.catch(e => console.log("promise_catch"))
//promise1_then
//promise_catch
(6)Promise.race()
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例:
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为Rejected,否则变为Resolved:
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为Rejected,从而触发catch方法指定的回调函数。
(7)Promise.prototype.finally()
1.finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作
该方法是 ES2018 引入标准的。
不管Promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数:
new Promise(function (resolve, reject) {
resolve();
})
.then(result => {
console.log(1)
})
.catch(error => {
console.log(2)
})
.finally(() => {
console.log(3)
});
//1
//3
new Promise(function (resolve, reject) {
reject();
})
.then(result => {
console.log(1)
})
.catch(error => {
console.log(2)
})
.finally(() => {
console.log(3)
});
//2
//3
finally方法的实现:
下面代码中,不管前面的 Promise 是Fulfilled还是Rejected,都会执行回调函数callback。
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 })
);
};
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器:
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
2.finally方法与then方法类似,但二者不相同:
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。
a、finally方法的回调函数不接受任何参数(而then方法的回调函数是接受参数的)
这意味着没有办法知道,前面的 Promise 状态到底是Fulfilled还是Rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
b、finally方法是不改变Promise的状态的(而then方法会改变Promise的状态)
let p1=Promise.resolve(1).then(()=>2,()=>3);
console.log(p1);
// Promise {
// [[PromiseStatus]]: "resolved"
// [[PromiseValue]]: 2
// }
// then返回了一个Resolved为2的Promise
let p2=Promise.resolve(1).finally(()=>2);
console.log(p2);
// Promise {
// [[PromiseStatus]]: "resolved"
// [[PromiseValue]]: 1
// }
// finally不改变原来Resolved的状态
let p3=Promise.reject(1).then(()=>2,()=>3);
console.log(p3);
// Promise {
// [[PromiseStatus]]: "resolved"
// [[PromiseValue]]: 3
// }
// then返回了一个状态为Resolved为3的Promise
let p4=Promise.reject(1).finally(()=>2);
console.log(p4);
// Promise {
// [[PromiseStatus]]: "rejected"
// [[PromiseValue]]: 1
// }
// finally不改变原来Rejected的状态
(8)应用
1.加载图片
我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
2.Generator函数与Promise的结合
使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。
unction getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
const it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());
}
run(g);
上面代码的 Generator 函数g之中,有一个异步操作getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。
(9)Promise.try()
不知道或者不想区分函数f是同步函数还是异步函数,但是希望有统一的写法,也就是说有一种统一的API,让同步函数同步执行,异步函数异步执行。
有两种写法:
//写法一
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next
//写法二
const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
1.写法一:第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步:
(async () => f())()
.then(...)
需要注意的是,async() => f()会吃掉f()抛出的错误。所以,如果想捕获错误,要使用Promise.catch方法:
(async () => f())()
.then(...)
.catch(...)
2.写法二:也是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。
提案:提供Promise.try方法替代上面的写法
鉴于这是一个很常见的需求,所以现在有一个提案,提供Promise.try方法替代上面的写法。
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
还可以统一用Promise.catch()捕获所有同步和异步的错误:
Promise.try(...)
.then(...)
.catch(...)
事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。
(10)Promise存在的一些问题
1.无法取消Promise,一旦新建它就会立即执行,无法中途取消。
2.如果不设置回调函数,Promise内部抛出的错误不会反应到外部。
3.当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。