- 声明:本文用于笔者前端学习阶段的知识点整理,如有遗漏与问题,请多指教,部分内容源自互联网,侵删。
1.基础知识
1.1 同步与异步代码
- 在了解什么是
promise之前,需要先了解什么是同步代码和异步代码 - 这里简单的举一个例子:
-
- 假如你去吃饭,要点单服务员却告诉你,你必须等待他服务完上一桌,才能给你服务,显然这体验极差.正常情况下,是你决定要点单了,会有另外一个服务员出来,给你服务,这是比较正常的情况.
- 当你点完单,服务员告诉你,现在大厨正在给上一桌客人做饭,你需要等大厨做完上一桌才能给你做,你才能吃上饭
- 这里映射回代码中
-
- 假如现在有一个定时器,设置
1w秒后执行,按照代码从上到下执行的顺序,如果要等这1w秒之后再执行后面的代码,显然是不现实的 - 所以我们需要将这个定时器,拿到一个位置,等待某个时间点执行其中的代码
- 假如现在有一个定时器,设置
-
上述这样的情况就产生同步代码和异步代码的概念
-
- 同步代码: 必须等到前面的同步代码执行完毕,得到结果才能执行下段同步代码.
- 异步代码: 不等待任务执行完毕,就执行其后面的代码.
1.2 异步代码的执行机制
- 浏览器中有
JS引擎来解析JS代码,JS引擎,会进行预解析 - 1.
JS引擎预解析的过程中,会将同步代码移动到主线程上排队执行
-
- 主线程:
JS中只有唯一的且为单向的线程,所有的代码都必须在这个线程上执行
- 主线程:
- 2.会将异步代码,移动到宿主环境中
-
- 宿主环境:由外壳程序创建与维护,只要能提供
js引擎执行的环境都可称之为外壳程序,例如:web浏览器等就是宿主环境
- 宿主环境:由外壳程序创建与维护,只要能提供
- 3.预解析其在解析异步代码时,会将异步代码分为宏任务与微任务
-
ES6规范中,microtask称为jobs,macrotask称为task
宏任务是由宿主发起的,而微任务由JavaScript自身发起.- 在
ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。
- 微任务有(不一一列举):
-
process.nextTick只有nodejs有promise.then().catch().finally()成功失败都会触发finally()中的回调,pending是不会触发的。
- 宏任务有(不一一列举):
-
setTimeout定时器setInterval延时函数script标签
-
4.
event loop(事件循环)
- 先上图,方便理解
- 它就是异步代码的执行机制,也就是等待宿主环境分配宏任务 , 反复等待 与 执行的过程就是事件循环。
Event Loop中,每一次循环称为tick,每一次tick的任务如下:
-
- 执行栈选择最先进入队列的宏任务(一般都是
script),执行其同步代码直至结束; - 检查是否存在微任务,有则会执行至微任务队列为空;
- 如果宿主为浏览器,可能会渲染页面;
- 开始下一轮
tick,执行宏任务中的异步代码(setTimeout等回调).
- 执行栈选择最先进入队列的宏任务(一般都是
1.3 抛出问题
- 这个时候就出现了一个问题,我们依旧是用一个现实的例子来引出
-
- 依旧去饭店去吃饭,我们点完菜之后,因为点的是一个需要挺长时间才能做好的菜,但是主食是提前做好的,所以先主食给先上了,但是我们需要有菜才能吃,这样对消费者的体验就不好
- 映照回代码;
-
- 假设现在页面中进行了两次
ajax请求,我们需要通过第一次ajax请求回来的数据发起第二次请求,但是由于第一次请求的数据很多,所以很慢,第二次请求没有等到第一次请求完成就发送了请求,这个时候就会出现明显的问题,第二次请求没有数据,就会请求出错
- 假设现在页面中进行了两次
- 解决方法;
-
- 解决这种问题的方法也很简单,就是等到第一个请求执行完毕,再执行第二个请求,就是使用一种嵌套的形式
- 缺点:
-
- 通过这种回调形式是可以解决异步代码执行中的不确定性的问题,但是随之带来的是复杂的嵌套格式,在代码较多的情况下,甚至会形成回调地狱
- 正当程序员们在苦恼回调地狱这个问题时,promise就诞生了
2.promise的使用
- 在明确使用方法前,先介绍一下promise的三种状态
- promise的状态不受外界影响 (3种状态)
-
- Pending状态(进行中)
- Fulfilled状态(已成功)
- Rejected状态(已失败)
- 1.创建
Promise实例
-
Promise构造函数接受一个回调函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。
-
-
resolve是将promise对象的状态从Pending状态,转换为Fulfilled状态,异步操作成功时,将结果作为参数传递出reject是将promise对象的状态从padding状态转换为Rejected状态,异步操作失败时,将结果传递出去
-
- 2.then方法
-
- 在promise实例生效后,可用
then方法分别指定两种状态回调参数.then方法可以接受两个回调函数作为参数:
- 在promise实例生效后,可用
-
-
- Promise对象状态改为Resolved时调用 (必选)
- Promise对象状态改为Rejected时调用 (可选)
-
// 基本用法
function sleep(ms) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, ms);
})
}
sleep(500).then( ()=> console.log("finished")); //500毫秒后执行.then中的参数
- 3.then方法中的return(重点)
-
- 之所以
promise可以解决回调地狱的问题,就在于.then方法中的return - 当
.then方法return另一个promise实例对象,就可以继续调用.then方法,而且我们刚才说明了.then方法中的结果都是代码执行结束的结果,这样就保证了上一个异步任务执行完毕,再执行下一个异步任务
- 之所以
- 4.catch方法
-
- 如果不在
.then中获取失败的信息,可以在.catch方法中获取 - 优势在于,可以获取所有
promise实例中的错误信息
- 如果不在
- 5.对于.then方法的执行顺序,这里贴一个实例
let promise = new Promise(function(resolve, reject){
console.log("1");
resolve();
});
setTimeout(()=>console.log("2"), 0);
promise.then(() => console.log("3"));
console.log("4");
// 1
// 4
// 3
// 2
- 代码在执行时 1 与 4是同步代码,执行顺序都懂,问题在于 2 与 3 的执行顺序
-
- 定时器是另一个宏任务,需要在任务队列等待执行
promise的.then是微任务,会在同步代码执行结束之后,自动执行
3.async 与 await
3.1 引出async与await
- 在解释async与await之前,先来体会一个案例
// 现在页面中有四次axios请求,要按照顺序调用
new Promise(function(resolve){
ajaxA("xxxx", ()=> { resolve(); })
}).then(function(){
return new Promise(function(resolve){
ajaxB("xxxx", ()=> { resolve(); })
})
}).then(function(){
return new Promise(function(resolve){
ajaxC("xxxx", ()=> { resolve(); })
})
}).then(function(){
ajaxD("xxxx");
});
- 上述的语法看起来是不是比较混乱,如果我们将axios请求封装成函数,代码看起来会简洁不少
// 将请求封装成一个对象
function request() {
// 省略代码....
return new Promise(...) // 需要返回一个Promise对象
}
// 代码改造
request("ajaxA")
.then((data)=>{
//处理data
return request("ajaxB")
})
.then((data)=>{
//处理data
return request("ajaxC")
})
.then((data)=>{
//处理data
return request("ajaxD")
})
- 代码简洁了许多,但是不够简洁,在这里我们先体验一下用async与await修改代码样式
async function load(){
await request("ajaxA");
await request("ajaxB");
await request("ajaxC");
await request("ajaxD");
}
- 现在这段代码是不是看起来就像同步代码一样,代码的阅读和编写都变得简单了,更重要的是代码的执行顺序也很清晰
- 所以,一句话就可以解释async和await的作用, 解决then方法的反复调用,编写冗余的问题
3.2 使用async与await
3.2.1async修饰函数
- async修饰的函数返回值的是Promise对象,当async函数没有返回值时,返回值是Promise.resolve(undefined)
// 有返回值
async function hello() {
return "hello async";
}
const res = hello();
console.log(res); // Promise {<fulfilled>: "hello async"}
// 无返回值
async function hello() {
"hello async";
}
const res = hello();
console.log(res); // Promise {<fulfilled>: undefined}
- 因为其返回的是一个Promise对象所以可以调用then方法
hello().then(res => {
console.log(res); // hello async
});
3.2.2await使用
- 1.await只能放在async修饰的函数内部使用,否则就会报错
- 2.await如果等到不是一个Promise对象,就返回它等到的东西,如果等到的是一个Promise对象,那await就会阻碍其后代码的执行,等待Promise的resolve的值,作为其返回值
// 看一看下面代码的执行顺序
console.log(1)
async function hello() {
console.log(2)
await setTimeout(()=>console.log(4),0)
console.log(3)
}
hello();
// 1 2 4 3
//由此可见 await 接收到promise对象后会阻碍后面的代码执行
3.2.3错误处理
- 在async修饰的函数中,无论是Promise reject的数据错误还是逻辑错误都无法显示,会被默默吞掉,所以想要得到错误最好的方式,就是去捕捉错误,可以用try{},catch{}的方式捕捉Promise对象中的reject中的错误
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {reject('error')}, ms); //reject模拟出错,返回error
});
}
async function res(ms) {
try {
console.log('输出了正确的内容');
await timeout(ms); //这里返回了错误
console.log('输出了错误的内容'); //所以这句代码不会被执行了
} catch(err) {
console.dir(err); //这里捕捉到错误error,并打印在工具台
}
}
res(1000);
3.2.4注意事项
- async和await属于ES8的新语法,兼容性很差,但是在我们自己做项目的时候可以借助babel来进行降级
- 安装依赖:
npm i babel-preset-es2015 babel-preset-stage-3 babel-runtime babel-plugin-transform-runtime
- 修改配置
"presets": ["es2015", "stage-3"],
"plugins": ["transform-runtime"]
这样就可以在项目中使用 async 函数了。