JS基础知识(五)- 异步进阶
# 1.几道面试题(以点带面)
1.请描述 event loop(事件循环/事件轮询)的机制,可画图
2.什么是宏任务和微任务,两者有什么区别?
3.Promise 有哪三种状态?如何变化?
4.场景题-Promise then 和 catch 的连接
//第一题
Promise.resolve().then(()=>{
console.log(1)
}).catch(()=>{
console.log(2)
}).then(()=>{
console.log(3)
})
2
3
4
5
6
7
8
//第二题
Promise.resolve().then(()=>{
console.log(1)
throw new Error('erro1')
}).catch(()=>{
console. log(2)
}).then(()=>{
console.log(3)
})
2
3
4
5
6
7
8
9
//第三题
Promise.resolve().then(()=>{
console. log(1)
throw new Error('erro1')
}).catch(()=>{
console.log(2)
}).catch(()=>{//这里是 catch
console.log(3)
})
2
3
4
5
6
7
8
9
5.场景题-async/await语法
//第一题
async function fn(){
return 100
}
(async function(){
const a = fn ( ) // ? ?
const b = await fn()//??
})()
2
3
4
5
6
7
8
//第二题
(async function(){
console. log('start')
const a = await 100
console.log('a',a)
const b = await Promise . resolve ( 200 )
console. log('b', b)
const c = await Promise.reject(300)
console. log('c', c)
console. log('end')
})()//执行完毕,打印出那些内容?
2
3
4
5
6
7
8
9
10
11
6.场景题-Promise和setTimeout的顺序
console. log(100)
setTimeout(()=>{
console. log(200)
})
Promise.resolve().then(()=>{
console.log(300)
})
console.log(400)
2
3
4
5
6
7
8
7.场景题-外加 async/await 的顺序问题
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1()
new Promise(function(resolve){
console.log('promise1')
resolve()
}).then(function(){
console.log('promise2')
})
console.log('script end')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 2.涉及知识点
Event Loop
Promise 进阶
async/await
微任务/宏任务
# 2.1 Event Loop(事件循环/事件轮询)
- JS是单线程运行的
- 异步要基于回调来实现
- Event Loop 就是异步回调的实现原理
JS代码如何执行
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
示例代码
console.log('Hi')
setTimeout(function cb1(){
console.log(apos;cb1')//cb即callback
},5000)
console.log('Bye')
2
3
4
5
下面我们借示例代码来分析Event Loop的具体机制
①从第一行代码开始执行,把它推入调用栈(Call Stack)中,由调用栈负责执行该代码,执行时控制台打印"Hi"

②此时第一行代码执行完毕,清空调用栈

③然后执行第二段代码,前文说过setTimeout是浏览器制定的标准Web API,而不是ECMA标准,所以Web APIs的栈里就多了一个5秒的定时器Timer,里面存放着setTimeout的第一个参数,也就是回调函数cb1

④此时调用栈执行第二段代码完毕,清空调用栈,然后马上再执行第三段代码,将其推入栈中执行,浏览器控制台打印"Bye"

⑤此时调用栈已清空,且所有同步代码执行完毕,所以Event Loop机制会马上启动(具体由浏览器内核管理),该机制会不停轮询,查看回调队列(Callback Queue)中有没有新的代码,有则将其推入调用栈执行。具体到示例,则是5秒钟后,定时器将回调函数cb1准时推入回调队列中

⑥此时Event Loop轮询发现回调函数中有新的代码,于是立即将其推入调用栈,调用栈直接执行回调函数cb1,由于cb1函数体内另有代码,所以调用栈的栈顶又多加了层console.log('cb1')
,执行后控制台输出"cb1"

⑦此时所有同步代码又一次全部执行完毕,调用栈重新清空,到此整个示例代码执行结束

# 总结 Event Loop 过程
- 同步代码,一行一行放在Call Stack执行
- 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
- 如Call Stack为空(即同步代码执行完)Event Loop开始工作
- 轮询查找 Callback Queue,如有则移动到 Call Stack 执行
- 然后继续轮询查找(永动机一样)
# DOM 事件和 Event Loop
- JS 是单线程的
- 异步(setTimeout,ajax等)使用回调,基于Event Loop
- DOM 事件也使用回调,也基于Event Loop机制(但DOM事件并不算异步)
# 2.2 Promise进阶
需要把握的三个要点:
- Promise的三种状态
- 状态的表现和变化
- then 和 catch 对状态的影响
# 三种状态
- pending resolved rejected
- pending -> resolved 或 pending -> rejected
- 变化不可逆
const p1 = new Promise((resolve, reject) =>{})
console.log('p1', p1) // pending
const p2 = new Promise((resolve, reject) =>{
setTimeout(()=>{
resolve()
})
})
console.log('p2',p2)// pending 一开始打印时
setTimeout(()=> console. log('p2-setTimeout', p2))// resolved
const p3 = new Promise((resolve, reject) =>{
setTimeout(()=>{
reject()
})
})
console.log('p3', p3)//一开始仍是pending,控制台展开后发现变成了rejected
setTimeout(()=> console. log('p3-setTimeout', p3))// rejected
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 状态的表现
- pending状态,不会触发then和catch
- resolved 状态,会触发后续的 then 回调函数
- rejected 状态,会触发后续的 catch 回调函数
# then 和 catch 改变状态
- then 正常返回 resolved,里面有报错则返回 rejected
- catch 正常返回 resolved,里面有报错则返回 rejected
/*本段代码演示:then 正常返回 resolved,里面有报错则返回 rejected*/
const p1 = Promise.resolve().then(()=>{
return 100//此时正常返回resolved状态
})
// resolved 会触发后续 then 回调
p1.then(()=>{
console.log('123')
})
const p2 = Promise.resolve().then(()=>{
throw new Error('then error')//此时返回rejected状态
})
// rejeced触发后续 catch 回调
p2.then(()=>{
console.log('456')
}).catch(err => {
console.error('err100',err)//于是只执行了这里的代码,then的回调函数不进入执行
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*本段代码演示:catch 正常返回 resolved,里面有报错则返回 rejected*/
const p3 Promise.reject('my error').catch(err =>{
console.error(err)
})
console.log('p3',p3)//resolved。注意!触发then回调
p3.then(()=>{
console.log(100)
})
const p4=Promise.reject('my error').catch(err =>{
throw new Error('catch err')
})
console.log('p4',p4)//rejected。触发 catch回调
p4.then(()=>{
console.log(200)
}).catch()=>{
console. log('some err')
})// p4执行完毕,状态最终返回的是resolved
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3 async/await
- 异步回调 callback hell
- Promise then catch 链式调用,但也是基于回调函数
- async/await是同步语法,彻底消灭回调函数
# async/await 和 Promise 的关系
- async/await是消灭异步回调的终极武器
- 但和 Promise 并不互斥,反而二者相辅相成
- 执行async函数,返回的是Promise对象
- await 相当于 Promise 的 then
- try...catch 可捕获异常,代替了 Promise 的 catch
示例代码
async function fn1(){
// return 200//相当于 return Promise.resolve(200)
return Promise.resolve(200)
}
const res1=fn1()//执行 async 函数,返回的是一个 Promise 对象
// console. log('res1', res1)// Promise 对象
res1.then(data =>{
console.log('data',data)//200
})
2
3
4
5
6
7
8
9
10
/*下面三段代码主要演示三种实质结果一样的情况:await 相当于 Promise 的 then*/
!(async function(){
const p1=Promise.resolve(300)
const data = await p1 // await 相当于 Promise then
console.log('data', data)//打印300
})()
!(async function (){
const datal = await 400 // 相当于await Promise.resolve(400)
console.log('datal', data1)//打印400
})()
!(async function (){
const data2 = await fn1()//await后面跟了个async函数,也就相当于跟了个Promise
console.log('data2', data2)//打印200
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
!(async function (){
const p4=Promise.reject('err1')//rejected状态
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)//try..catch相当于promise catch
}
})()
/*这段代码演示了Promise状态不为resolved,await无能为力的特殊情形*/
/*这也说明了try..catch语法和await配合的必要性*/
!(async function()
const p4=Promise.reject('err1')//rejected 状态
const res = await p4 // await -> then
console.log('res', res)//该行代码未被执行
)()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 异步的本质
- async/await是消灭异步回调的终极武器
- JS还是单线程,还得是有异步,还得是基于event loop
- async/await只是一个语法糖,但这颗糖真香!
async function async1(){
console.log('async1 start')//第2步打印
await async2()// 执行结果是undefined
// await 的后面,都可以看做是 callback 里的内容,即异步
//类似于event loop,setTimeout(cb1)。但setTimeout是宏任务
//实际await后面的代码应该等于微任务,即类似于Promise.then,后面会讲
//setTimeout(function(){console.log('async1 end')})
//Promise.resolve().then( ()=>{console.log('async1 end')} )
console.log('async1 end')//第5步打印
}
async function async2 (){
console.log('async2')//第3步打印
}
console.log('script start')//第1步打印
async1()
console. log('script end')//第4步打印
//此时同步代码已经执行完(event loop机制,开始执行异步任务)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function async1(){
console. log('async1 start')//2
await async2()
//下面三行都是异步回调 callback 的内容
console.log('async1 end')//5
await async3()
//下面一行是异步回调的内容
console. log('async1 end 2')//7
}
async function async2 (){
console. log('async2')//3
}
async function async3(){
console.log('async3')//6
}
console.log('script start')//1
async1()
console. log('script end')//4
//同步代码执行完 event loop
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2.4 for ... of
- for...in(以及forEach、for)是常规的同步遍历
- for...of 常用于异步的遍历
function muti(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num * num);
}, 1000);
});
}
var arr = [1, 2, 3];
//如果使用foreach遍历处理异步代码不会等待异步代码的执行,一次输出所有异步结果
// arr.forEach(async (value, index, array) => {
// // console.log(value);
// const val = await muti(value);
// console.log(val);
// });
//如果使用for...of可以使异步按先后顺序执行
!(async function () {
for (let a of arr) {
const ans = await muti(a);
console.log(ans);
}
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2.5 宏任务和微任务
宏任务:MacroTask,微任务:MicroTask
需要把握的三个要点:
- 什么是宏任务,什么是微任务
- event loop 和 DOM 渲染
- 微任务和宏任务的区别
# 二者分类
- 宏任务:setTimeout,setInterval,Ajax,DOM事件
- 微任务:Promise async/await
- 微任务执行时机比宏任务要早
# Event Loop 和 DOM 渲染
- JS是单线程的,而且和DOM渲染共用一个线程
- JS执行的时候,得留一些时机供DOM渲染
回顾2.1节Event Loop过程的示例代码,此时我们可以增加 DOM 渲染时机,并给出如下示意图

总结
- 每次 Call Stack清空(即每次轮询结束),即同步任务执行完
- 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
- 然后再去触发下一次 Event Loop
# 微任务和宏任务的区别
- 宏任务:DOM 渲染后触发,如 setTimeout
- 微任务:DOM渲染前触发,如Promise
# 从 Event Loop 解释,为何微任务执行更早
- 微任务有个专门的队列micro task queue,它和宏任务队列,也就是图中所示Callback queue是分开的
- 这是因为微任务是ES6语法规定的,宏任务是由浏览器规定的


# 3.对前文问题的解答
# 3.1 描述 Event Loop 机制(可画图)
- 自行回顾前文 Event Loop 的过程
- 和DOM渲染的关系(可以分段,逐步讲,不然有点乱)
- 微任务和宏任务在 Event Loop 过程中的不同处理
# 3.2 什么是宏任务和微任务,两者区别
- 宏任务:setTimeout,setInterval,Ajax,DOM事件
- 微任务:Promise,async/await
- 微任务执行时机比宏任务要早
# 3.3 Promise 有哪三种状态?如何变化?
- pending resolved rejected
- pending -> resolved 或 pending -> rejected
- 变化不可逆
# 3.4 场景题-Promise then 和 catch 的连接
//第一题
Promise.resolve().then(()=>{
console.log(1)//第一步执行,打印1
}).catch(()=>{
console.log(2)//不进入执行
}).then(()=>{
console.log(3)//第二步执行,打印3
})
//第二题
Promise.resolve().then(()=>{
console.log(1)//第一步执行,打印1
throw new Error('erro1')//第二步执行,变成rejected状态
}).catch(()=>{
console.log(2)//第三步执行,打印2,变成resolved状态
}).then(()=>{
console.log(3)//第四步执行,打印3,继续resolved状态
})
//第三题
Promise.resolve().then(()=>{
console.log(1)//第一步执行,打印1
throw new Error('erro1')//第二步执行,变成rejected状态
}).catch(()=>{
console.log(2)//第三步执行,打印2,变成resolved状态
}).catch(()=>{//这里是 catch
console.log(3)//不执行
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 3.5 场景题-async/await语法
//第一题
async function fn(){
return 100
}
(async function(){
const a = fn () // 结果返回Promise,值是100
const b = await fn()//await相当于Promise的then,所以b就等于100
})()
//第二题
(async function(){
console.log('start')
const a = await 100//相当于直接返回值
console.log('a',a)//打印100
const b = await Promise.resolve (200)//牢记await相当于Promise的then方法
console. log('b', b)//打印100
const c = await Promise.reject(300)//这里直接报错,后续代码都不会执行
console.log('c', c)
console.log('end')
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.6 场景题-Promise和setTimeout的顺序
console.log(100)//第一步执行,打印100
setTimeout(()=>{
console.log(200)//第四步执行,打印200
})
Promise.resolve().then(()=>{
console.log(300)//第三步执行,打印300
})
console.log(400)//第二步执行,打印400
2
3
4
5
6
7
8
# 3.7 场景题-外加 async/await 的顺序问题
async function async1(){
console.log('async1 start')//第2步执行
await async2()
//await后面的代码都作为回调内容-微任务
console.log('async1 end')//第6步执行
}
async function async2(){
console.log('async2')//第3步执行
}
console.log('script start')//第1步执行
setTimeout(function(){
console.log('setTimeout')//宏任务,第8步执行
},0)
async1()
//初始化Promise时,传入的函数会被立即执行
new Promise(function(resolve){
console.log('promise1')//第4步执行
resolve()
}).then(function(){
console.log('promise2')//微任务,第7步执行
})
console.log('script end')//第5步执行
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26