什么是promise呢?就是异步编程的一种解决方案。简单来看,可以把它看作一个容器,里面存放着某个未来才会结束的事件的结果。从语法上来说,它是一个对象,可以从它来获取异步操作的消息
这样可能好理解一些
比如去KFC买了一个汉堡,通过下订单并付款,此时就是发送了一个对某个值(就是那个汉堡)的请求。这时,服务员会给你一个排号,这个排号就是一个promise,保证你能拿到那个值(那个汉堡)。拿到那个排号后,你就可以做其他事情了,不必一直等到服务员给你汉堡。在你做其他事情时,服务员喊你的排号让你过去,此时可能会给你一个汉堡,然后你把排号给他,这是成功的结果,就是你拿到了你所请求的值。另一种可能是告诉你汉堡没了,让你换一个,然后你把排号给他,这是失败的结果,你没有拿到你所需要的值。但是,不管如何,只要你有这个排号(promise),那么就一定会有一种结果,成功或失败。而且,这种结果一旦产生就不会改变
了解了这点,应该能理解promise对象接受一个函数作参数,这个函数又接受两个函数作为参数
let promise=new Promise(function(resolve,reject){
//...code
if(/*异步成功*/){
resolve(value);
}else{
reject(error);
}
});
再接着上面的故事,当你拿到汉堡后,你就可以离开KFC,然后去做其他事情。或者你没有拿到汉堡,然后你又重新选了另一种食物。不管如何,在你得到其中一种结果后,你都会有相应的对策。这种处理接收到的值(汉堡)的应对方案就是then()方法,这个方法也是有两个函数作为参数,分别对应拿到汉堡和没拿到汉堡的情况
promise.then(
(value) => {
//success 拿到汉堡
},
(error) => {
//failure 没拿到汉堡
});
这样子应该很好理解这个promise
那么先来简单使用一下
let promise=new Promise((resolve,reject) => {
console.log("Promise");
resolve();
});
promise.then(() => {
console.log("Resolved");
})
console.log("Hi!");
//Promise
//Hi!
//Resolved
Promise新建后会立即执行,所以首先输出的是Promise。然后then方法指定的回调函数会在所有的同步任务执行完成之后再执行,所以先输出Hi!,最后再输出Resolved
一定要注意,promise对象中的函数会立即执行,而then()方法中的回调函数会异步执行
对于这个promise对象。其实,并不一定要是通过new出来的。就像那个排号,并不一定非得是拿个排号才能取到汉堡,也可能是拿一张汉堡兑换券去取汉堡,也就是说只要具有能换取汉堡的特征都能被当成是一个promise。那么promise对象也是如此,如何区分呢,就是可以认为只要有then()方法的函数或对象都可以看成是一个promise
比如下面这些对象
var obj={
then(){}
}
var v=Object.create(obj);
Object.prototype.then=function(){};
var v1={ hello:"hello world"};
obj,v,v1都会被认为是一个promise对象。所以当对一个对象或函数定义方法时要注意
那么这是如何检测出来的呢?大致可以类似于下面这样
if(
p !== null &&
(typeof p ==="object" ||typeof p === "function") &&
typeof p.then === "function"
){
//可以把P当成promise来使用
}else{
//不可以把P当成promise来使用
}
但是呢,这里还是有一个小小的区分,通过new出来的叫promise实例,而通过添加then()方法而被认为是promise对象的叫thenable对象
至于如何使用promise对象,就以api的形式来细说
promise.resolve()方法
Promise.resolve()
//等价于
new Promise((resolve,reject) => resolve());
接收一个参数,返回一个安全的结果为Resolved的promise对象
//参数为Promise对象,则直接返回这个对象,不做任何修改
let p=new Promise((resolve,reject) => {
resolve("成功了");
});
Promise.resolve(p)
.then((value) => {
console.log(value);
});
//成功了
//参数为thenable对象,则会将这个对象转换为Promise对象,立即执行该对象
//中的then()方法
let thenable={
then(resolve,reject){
resolve(42);
}
}
let p=Promise.resolve(thenable);
p.then((value)=>{
console.log(value);
});
console.log("now");
//now
//42
//参数不具有then方法或者不是对象,则返回一个由该值填充的promise对象,
//结果为Resolved
setTimeout(()=>{
console.log("later");
},0);
let p=Promise.resolve(5);
p.then((value)=>{
console.log(value);
});
console.log("now");
//now
//5
//later
//当不带有参数时,直接返回一个Resolved结果的promise对象(所以,
//如果希望得到一个promise对象,可直接调用该方法)
setTimeout(()=>{
console.log("three");
},0);
Promise.resolve().then(()=>{
console.log("two");
});
console.log("one");
//one
//two
//three
promise.reject()方法
Promise.reject()
//等价于
new Promise((resolve,reject) => reject());
接收一个参数,返回一个结果为Rejected的promise对象
//参数为promise对象,直接返回这个对象
let p=Promise.reject("出错了");
p.then(null,(value) =>{
console.log(value);
});
//出错了
//参数为thenable对象,注意这里的返回值是thenable对象,而不是
//执行then方法
let thenable={
then(resolve,reject){
reject("出错了");
}
}
Promise.reject(thenable)
.then(null,(value) =>{
console.log(value);
});
//{then: ƒ}
//参数不具有then方法或者不是对象,则返回一个由该值填充的promise对象,
//结果为Reject
Promise.reject(4)
.then(null,(value) =>{
console.log(value);
});
//4
可以看出,resolve和reject方法其实没多大区别,只是当处理参数为thenable对象时方式不同,这点要注意
promise.then()方法
这个方法接收两个参数,第一个参数是结果为Resolved的回调函数,第二个是结果为Rejected的回调函数。返回一个新的promise对象。不管从这个then方法返回的是什么值,都会被自动链接到下一个then方法并作为其参数。对于第二个回调函数,如果没有写,则会有一个默认的拒绝处理函数顶替
let p=Promise.reject("出错了");
p.then(function resolve(){
console.log("成功了");
})
.then(null,(value) => {
console.log(value);
});
//出错了
默认的拒绝处理函数只是会把错误重新抛出,并不会进行处理。本质上来说,这个错误会一直沿着这个promise链传播下去,直到遇到一个显示定义的拒绝处理函数来处理这个错误
那么现在写一个简单的promise链
function delay(time){
return new Promise((resolve,reject) =>{
setTimeout(resolve,time);
});
}
delay(100)
.then(function step2(){
console.log("step 2 after 100ms");
return delay(200);
})
.then(function step3(){
console.log("step 3 after 100ms");
return delay(300);
});
//step 2 after 100ms
//step 3 after 100ms
promise.catch()方法
p.catch((error) => { console.log(error) });
//等同于
p.then(null,(error) => { console.log(error) });
接收一个参数,同时返回新的promise对象
专门用来处理异步操作过程中抛出的错误
let p=new Promise((resolve,reject) => {
reject("失败了");//也可以写成throw new Error()
})
p.then(() => {
console.log("one");
})
.catch((value) => {
console.log(value);
return value;
})
.then((value) => {
console.log(value,"two","成功了");
})
//失败了
//失败了 two 成功了
这种错误是能被catch所捕获的,这是因为这个错误就代表了promise的结果。但是,如果在有了promise结果之后还抛出一个错误,那么这个错误就不会被捕获,也不会被传递到外层代码中,即不会打断整个程序的运行。至于为什么不能被捕获,是因为promise已经有了结果,不可更改,具有不可变性,如果此时再去捕获,则说明promise结果被更改,违反了该规则
let p=new Promise((resolve,reject) => {
resolve("成了");
throw new Error("失败了");
})
.then((value) => {
console.log(value);
})
.catch(() => {
console.log("我捕获到了");
})
setTimeout(() => {
console.log("我是外层代码运行结果");
},0);
//成了
//我是外层代码运行结果
这就是在有了promise结果之后再抛出错误,但是没有被catch所捕获,同时也没有传递到外层代码中,只是留在了promise链中
还有一种错误也是无法被catch所捕获的
let p=new Promise((resolve,reject) => {
setTimeout(function(){
throw new Error("test")
},0);
})
p.then((value) => {
console.log("我成了");
})
.catch((value) => {
console.log(value);
})
setTimeout(() => {
console.log("我是外层代码")
},0);
console.log("a");
//a
//Uncaught Error: test
//at 18.html:357
//我是外层代码
这个promise是运行在本轮的事件循环中,setTimeout是运行在下一轮的事件循环之前开始。而promise指定在下一轮事件循环再抛出错误,则无法被本轮的事件循环中catch所捕获,所以在本轮的promise链中不会执行任何代码,当然也不会打断程序的运行。但是这里的错误会传递到最外层,这点与之前不同。为什么是传递到最外层,因为不是在本轮事件循环中报错
所以,到这里可以清楚一点,就是只要是在promise链中抛出错误,都不会打断程序的运行。而所抛出的错误会一直传递下去,直到遇见显示拒绝处理函数来处理(catch或then方法中的第二个参数)。如果promise链中没有报错,则这个catch会被忽略
promise.all()方法
接收一个数组参数(也不一定是数组,但是必须具备Iterator接口,这个下下篇写),通常由Promise实例组成(也不一定,也可以是promise,thenable对象和立即值,如果是这些,则都通过promise.resolve方法进行包装成为一个promise对象,如果是一个空数组则会立即完成)。从这个promise.all()返回的主promise在且仅在所有的promise成员都完成之后才完成。如果这些promise中有任何一个结果为Rejected的,则这个主Promise就会立即被拒绝,并丢弃所有来自其他promise的全部结果
//所有promise成员都完成了
let p=Promise.resolve("成了");
let thenable={
then(resolve){
resolve("成了");
}
}
Promise.all([p,thenable,20])
.then((value) => {
console.log(value);
})
.catch((err) => {
console.log(err)
})
//["成了", "成了", 20]
//有一个promise是Rejected
let p=Promise.resolve("成了");
let s=Promise.reject("没了");
let thenable={
then(resolve){
resolve("成了");
}
}
Promise.all([s,p,thenable,20])
.then((value) => {
console.log(value);
})
.catch((err) => {
console.log(err)
})
//没了
这些都还是很好理解的。但是有一个问题,如果传入数组的promise中已经定义了catch方法,而在promise.all()中也定义了catch方法,那么会先调用谁的呢?
let p=new Promise((resolve,reject) =>{
resolve("hello");
})
.then(result => result)
.catch(e => e);
let q=new Promise((resolve,reject) => {
throw new Error("出错了");
})
.then(result => result)
.catch(e => e);
Promise.all([q,p])
.then(result => console.log(result))
.catch(err => console.log(err));
//[Error: 出错了 "hello"]
上面这段代码应该能看懂吧。p是结果为Resolved的promise对象,q首先会是结果为Rejected,然后调用自身定义的catch方法,返回一个新的结果为Resolved的promise对象并赋值给q。所以在all()中的promise对象都是结果为Resolved的,直接调用all()下面的then方法,而忽略catch方法
还有一段和这个很相似的代码
let p=Promise.resolve("hello world");
p.then(result => result)
.catch( err => err)
let q=Promise.reject("出错了");
q.then(result => result)
.catch( err => err)
Promise.all([p,q])
.then((result) => {
console.log(result);
})
.catch( (err) => {
console.log(err);
})
//出错了
这段代码废了我好长一段时间,还是没能搞懂为什么和书上写的不一样,和MDN文档的也不一样。后来才发现是我写错了,不仔细看还真的不容易发现。谨记谨记!!!
promise.race()方法
该方法与all类似,接收一个数组参数,返回一个新的promise对象。但是,当数组中的promsie成员中任何一个最先获得最终结果(成功或失败),则race()方法返回这个promise对象
let p=new Promise((resolve,reject) =>{
setTimeout(() => {
resolve("我先执行啦");
},1000)
})
function timeOut(){
return new Promise((resolve,reject) => {
setTimeout(() => {
reject("你超时啦,不能再执行了")
},100)
})
}
Promise.race([p,timeOut()])
.then((value) => {
console.log(value);
})
.catch((err) => {
console.log(err);
})
//你超时啦,不能再执行了
这个例子很简单,p是延迟1m执行resolve函数,返回一个结果为Resolved的promise对象,而timeOut函数则是在100ms执行reject函数,返回一个结果为Rejected的promise对象。谁先执行,race方法就返回谁的对象。很显然是timeOut函数先执行,则会抛出错误,由race定义的catch捕获错误
还有一种比较常遇到的情况,就是不管函数f是不是同步还是异步操作,就想用Preomise来操作这个函数。这样就可以使用then方法来指定下一步的流程,或者用catch来捕获错误。但是,如果只是直接在then方法中调用函数f则会有一些小问题产生,比如,这个函数f是同步函数,当在then方法中时则变为了异步执行,这可能不是你想要的结果
let f= () => console.log("now");
Promise.resolve()
.then(f);
console.log("later");
//later
//now
那么,有没有解决方案呢?让同步函数任是同步执行,异步函数则异步执行有,且有两种(看看就行,不懂没关系,我也不懂)
第一种
使用async函数
let f= () => console.log("我是同步函数");
let p= setTimeout(() => console.log("我是异步函数"), 10);
(async () => f())();
(async () => p)();
console.log("wo");
//我是同步函数
//wo
//我是异步函数
第二种
使用new Promise()
let f= () => console.log("我是同步函数");
let p= setTimeout(() => console.log("我是异步函数"), 10);
(() => new Promise(
resolve => resolve(f()))
)();
( () => new Promise(
resolve => resolve(p))
)();
console.log("wo");
//我是同步函数
//wo
//我是异步函数
到这里就差不多介绍完了Promise函数,最少能基本使用了。还有很多东西没写出来,有些知识看了书也不会=,=
我觉得这个Promise最关键的地方就是每个方法都会返回一个新的Promise对象,以此不断链接下去。然后通过两种回调方法来处理成功或失败的情况,最后还有一个catch来捕获抛出的错误
这个返回新的Promise对象很重要。如果只是单纯的使用回调函数,那么就无法判断这个回调函数是否被调用了,或者调用了几次,又或是提前调用和调用过晚,这些情况都无从知晓,因为没有一个凭证来证明。但是Promise的出现就比较完美的解决了这些可能发生的问题。比如判断是否被调用,可以用promise.race()来判断,在一定时间内未执行则抛出一个自定义的错误来判定该函数未执行。调用几次的问题也不会发生,在promise链中每个then只会执行一次即返回一个新的promise对象。然后调用过早或过晚也是不太可能发生的,因为then方法是异步执行的,不会在同步函数之前被执行,但是下一个异步时机点(我的理解是在同步执行完后)上被依次立即执行
PS:
js是单线程的
事件循环:将所有代码按照块来区分,每次执行一个代码块,这就是事件,每执行完一个代码块,立即执行下一个块,这是循环
任务队列:每个块中所要执行的代码按照任务来区分,不同代码执行不同任务,总是先执行同步任务。队列就是按照顺序来挨个执行
这两个有很明显的区别,可以这么看:在游乐园玩一个游戏,事件循环就是在玩完一个游戏后重新排在最后面来重新玩,而任务队列就是玩完之后可以插队接着玩,能一直玩下去
为什么要说这些,因为promsie的异步特性是基于任务的,就是它能一直插队来执行
不理解没关系,我也不是很懂
下一篇,生成器,迭代器,有点难懂啊