前言
看着自己的简历,感觉像个实践派。看上去做了好多好多项目,自己确真的不了解Promise。每次面试官问道,我感觉都是胡乱答一通。为此我决定好好的去思考这个问题,结合各路大神的文章,我自己好好的搬运一回。
一、Promise 的出现?
话不多说,我们先看一个例子
请求1((请求结果1)=>{
处理结果1
})
但是如果是这样呢
请求1((请求结果1)=>{
请求2((请求结果2)=>{
请求3((请求结果3)=>{
......
})
})
})
实际的一个例子如下,一个内嵌H5活动页。
一进页面,--->先获取app端回调的用户信息,拿到uuid,-->马上去请求商品列表-->再去点进去后买。
getUserInfo->getGoodsList->pay
getUserInfo((res)=>{
getGoodsList((res.uuid,(good))=>{
pay((goods.uuid,res.uuid)=>{
......
})
})
})
实际上业界俗称的,地狱回调就出现了。那么解决问题就有了Promise。
二、请简单说一下Promise是什么?
简单来说就是一个容器,里面保存着某个未来才会结束的事件的结果。从语法层面来说,Promise实际是个对象,从它可以获取异步操作的消息。
再提炼来说,实际上就是解决异步变成的一种方案。
那么对于上述的问题,解决方案就有了
new Promise((请求1){
}).then((请求2){
请求结果1
}).then((请求3){
请求结果2
}.then((请求4){
请求结果3
}).cath((处理异常){
捕捉异常
})
三、基本用法
3.1、官方给出的基本用法如下:
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
1、Promise构造函数接受一个函数(executor)作为参数,这个函数的参数有两个,resolve(未完成-->成功(resolved))、reject(未完成-->失败(rejected))
2、Promise构造函数执行-->立即调用executor函数-->resolve,reject两个函数作为参数传递给executor函数
3、调用resolve函数后,一切正常(resolved),promise.then
4、调用reject函数后,抛出异常(rejected)promise.catch
平时使用Promise基本结构
const a = new Promise((resolve, reject) => {
setTimeout(() => {
console.log("你好");
}, 1000);
console.log("你好1");
});
a.then((res)=>{
//some code
})
a.catch((err)=>{
//some code
})
a.finally(()=>{
})
接下来就看个简单的例子:
const a = new Promise((resolve, reject) => {
setTimeout(() => {
console.log("你好");
}, 1000);
console.log("你好1");
/* executor */
});
console.log("你好2"); // 你好1 你好2 你好
Promise -->回调延迟绑定,接着👇代码
let p1 = new Promise((resolve, reject) => {
console.log(1);
resolve("测试");
console.log(2);
});
p1.then(
result => {
console.log("成功 " + result);
},
reason => {
console.log("失败 " + reason);
}
);
console.log(3);
// 1 2 3 成功 测试
Promise新建后立即执行,所以首先输出的是1、2 ,Promise在执行resolve时,触发微任务,将在当前脚本所有同步任务执行完才会执行,p1.then,存储起来两个函数,所以接下来输出的是3,再接下来就是then方法里面成功回调,输出成功,最后就是执行resolve(),测试
3.2、错误处理:
return new Promise((resolve, reject) => {
apiPromise //封装接口请求
.then((resp) => {
//成功逻辑
} else {
// 失败逻辑
}
}).then(()=>{
//todo
})
.catch(reject){
//捕获异常操作
};
});
不管是多层嵌套调用then方法,都可以通过最后一个对象.catch() 来捕获异常。
3.3、(重要)接着上面继续梳理,Promise链式调用,参考简书一博主的一个例子,如下
new Promise((resolve, reject) => {
console.log("log: 外部promise");
resolve();
})
.then(() => {
console.log("log: 外部第一个then");
new Promise((resolve, reject) => {
console.log("log: 内部promise");
resolve();
})
.then(() => {
console.log("log: 内部第一个then");
})
.then(() => {
console.log("log: 内部第二个then");
});
})
.then(() => {
console.log("log: 外部第二个then");
});
// log: 外部promise
// log: 外部第一个then
// log: 内部promise
// log: 内部第一个then
// log: 外部第二个then
// log: 内部第二个then
对于运行后的结果,我还是比较懵逼的。
参照资料慢慢梳理:
1、最先输出的log:外部promise,是因为每当new一个Promise实例的时候,除了会得到一个promise实例之外,会立即执行executor函数,所以就会马上输出。
2、接着resolve()被调用,然后就会执行外部的第一个then(),所以就会输出log:外部第一个then。与此同时外部的第二then(),将他们依次放入微任务队列中。
3、再接下来,又创建了new一个Promise实例,里面的运行机制上面1、2所述,所以就会输出log:内容promise、、log: 内部第一个then。
4、最后在按顺序一次执行被挂起的微任务队列中的任务log: 外部第二个then、log: 内部第二个then。
总结
执行then方法时,如果前面的promise已经是resolved,则直接将回调放入微任务队列中
new Promise((resolve, reject) => {
resolve("测试");
})
.then(() => {
console.log("log: 外部第一个then");
})
.then(() => {
console.log("log: 外部第二个then");
});
当一个 promise 被 resolve 时,会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中
四、Promise常用方法
4.1、Promise.all()\Promise.race()
Promise.all、Prmise.race 方法将多个Promise实例,包装成一个新的Promise实例
const p = Promise.all([p1, p2, p3]);
Promise.all--> 上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
const p = Promise.race([p1, p2, p3]);
Promise.race--->p1,p2,p3,之中有一个实例 率先改变状态,p的状态就跟着改变。
五、Promise 宏任务\微任务\事件轮询(Event-Loop)
5.1、宏任务、微任务都表示异步任务的两种分类。例如,经常在面试题、各种大咖博文里的代码片段:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
// 1 2 3 4
上文中,setTimeout,就是用来储存宏任务的,实例化new Promise,执行的代码片段都是同步进行的,而then中注册的回调是异步执行的。也就是说同步代码执行过后,才会去检查是否有异步任务完成,并执行相应的回调。注意微任务的执行是在宏任务之前。
5.2、常见宏任务、微任务分类:
- 常见宏任务:setTimeout、setInterval、setImmediate....
- 常见微任务:Promise.then catch finally、setImmediate.....
5.3、事件轮询(Event-Loop):
- 什么是Event-Loop,JS是一个单进程的语言,同一时间不能处理多个任务,所以何时执行宏任务,何时执行微任务,就要有一个判断逻辑的存在。也就是说,宏任务队列--->挂起--->微任务1、微任务2、......--->顺序执行 。
六、宏任务、微任务常见的的几种情况分析
6.1、主线程中创建宏任务和微任务,感觉上述的例子依旧还能说明
setTimeout(_ => console.log(4)) // 将回调代码放入挂机放到另一个宏任务队列
new Promise(resolve => {
console.log(1)
resolve()
}).then(_ => {
console.log(3) // 将回调代码放入微任务队列
})
console.log(2)
// 1 2 3 4
执行任务顺序:主线程-->主线程上创建的微任务-->主线程上创建的宏任务
6.2、微任务中创建微任务
setTimeout(_ => console.log(4));
new Promise(resolve => {
resolve();
console.log(1);
}).then(_ => {
console.log(3);
Promise.resolve()
.then(_ => {
console.log("第一次");
})
.then(_ => {
Promise.resolve().then(_ => {
console.log("第二次");
});
});
});
console.log(2);
//1 2 3 第一次 第二次 4
执行任务顺序:上述例子上来说,依旧还是先执行:主线程-->主线程创建的微任务1-->微任务2-->....主线程创建的宏任务
6.3、宏任务中创建微任务
setTimeout(() => {
console.log("最外层宏任务1");
setTimeout(() => {
console.log("最外层宏任务2");
}, 0);
new Promise(resolve => {
resolve();
console.log(1);
}).then(_ => {
console.log(3);
Promise.resolve()
.then(_ => {
console.log("第一次");
})
.then(_ => {
Promise.resolve().then(_ => {
console.log("第二次");
});
});
});
}, 0);
console.log(2);
//2 最外层宏任务1 1 3 第一次 第二次 最外层宏任务2
定论
- 1、微任务队列优于宏任务队列执行。
- 2、微任务队列上创建的宏任务会被后添加的当前宏任务队列的尾端。
- 3、微任务队列上创建的微任务会被添加到微任务队列的尾端。
- 4、只要微任务队列中还有任务,宏任务队列就会等待微任务队列执行完毕后在执行。
七、关于Promise的问题汇总
1、了解Promise吗?
2、Promise 如何使用?
3、Promise 常用的方法有哪些?它们的作用是什么?
4、Promie出现的原因?
参考文章
面试精选之Promise
你真的懂Promise吗
Promise 链式调用顺序引发的思考
这一次,彻底弄懂 JavaScript 执行机制