# JS 异步进阶(大厂)
# 1. 简述
- 本章主要讲JS异步的原理和进阶
- 符合一线互联网公司基层员工必须要求掌握的知识点
- 几乎也算是面试必考内容之一
# 2. 题目
# 2.1. 问答题
- 请描述event loop(事件循环/事件轮询)的机制,可画图
- 什么叫事件循环
- event loop在异步中起到什么作用,和异步有什么关系
- 什么是宏任务和微任务,两者有什么区别
- Promise有哪三种状态?如何变化
# 2.2. 场景题
- promise then和catch的连接(经常考)
- 经常考到,业务中可能会用,答不出来会减分很多
// 第一题 Promise.resolve().then(() => { console.log(1) }).catch(() => { console.log(2) }).then(() => { console.log(3) }) // 第二题 Promise.resolve().then(() => { console.log(1) throw new Error('erro1') }).catch(() => { console.log(2) }).then(() => { console.log(3) }) // 第三题 Promise.resolve().then(() => { console.log(1) throw new Error('erro1') }).catch(() => { console.log(2) }).catch(() => { console.log(3) })
1
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 - async/await语法问题
// 题目一 async function fn() { return 100 } (async function() { const a = fn() // ?? const b = await fn() // ?? }) // 题目二 (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') }) // 执行完毕,打印出哪些内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 - promise和setTimeout的顺序
- 经典题
- 常考,答不出来有些危险
console.log(100) setTimeout(()=> { console.log(200) }) Promise.resolve().then(()=> { console.log(300) }) console.log(400)
1
2
3
4
5
6
7
8 - 外加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() // async1 start - async2 - async1 end - script start - setTimeout ? new Promise(function(resolve) { console.log('promise1') resolve() }).then(function() { console.log('promise2') }) console.log('script end'); // 依次打印什么内容 //
1
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
# 3. 知识点
# 3.1. event loop(事件循环/事件轮询)
# 3.1.1. 基础概念
- 简述
- 偏原理,几乎必问
- JS是单线程运行的
- 异步要基于回调来实现
- event loop就是异步回调的实现原理
- JS如何执行 => 有助于梳理event loop过程
- JS代码它是从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
- 通过回调执行异步
- 回调如何实现 => 事件循环机制
- 示例
- event loop的流程比较复杂,我们可以通过简单的示例来进行梳理
console.log('Hi') setTimeout(function cb1() { console.log('cb1') // cb即callback }, 5000) console.log('Bye');
1
2
3
4
5
6
7 - 方法
- 不要扣细节,不要扩大范围,核心是event loop的过程
- 开始讲解事件循环过程
- 图示1 => 最初的状态
- Browser console => 浏览器环境console界面
- Call Stact => js运行中重要的模块
- 调用栈
- Web Apis => => js运行中重要的模块
- ECMA规范之外,浏览器定义的api相关(DOM/BOM)
- Callback Queue => js运行中重要的模块
- 回调函数的队列
- Event Loop
- 一遍一遍循环事件
- 开始执行第一行代码
console.log('Hi')
- js会把代码推入调用栈中,Call Stact会去执行这个代码
- 调用完之后在浏览器环境console界面打印hi
- 代码执行完毕之后清空调用栈
- 执行第二行代码
setTimeout cb1
- setTimeout是一个函数,我们执行这个函数的时候
- 即,将这个setTimeout cb1函数(三行)放到调用栈中
- 但控制台不打印,因为暂时还执行不到setTimeout里面的console
- 执行setTimeout函数的时候,由于setTimeout是浏览器定义的
- setTimeout的第一个参数cb1(异步方法)被放到了Web Apis中的定时器里面
- 其实遇到setTimeout他的产出就是把方法放到定时器里面,里面是什么方法不管
- 定时器的时间是5s,5s之后他把这个cb1放到Callback Queue(回调函数队列)中
- 此时setTimeout cb1这个方法就结束了
- 代码执行完毕之后清空调用栈
- 执行最后一行代码
- 和执行第一行代码一样
- 执行完最后一行代码之后
- 此时这个timer定时器还在web apis里面
- 此时代码已经全部执行完了,即调用栈中没有东西可以执行了
- 一旦调用栈空了,这时候会启动Event Loop机制
- 即所有的同步代码执行完,调用栈空了
- 浏览器内核启动事件循环机制
- 启动事件循环机制
- 事件循环机制会一遍一遍地进行循环
- 每次循环他会从Callback Queue(回调函数队列)中去找有没有函数,有函数的话就拿过来
- 拿过来之后再放到Call Stact(调用栈)中执行
- 5s还没到,回调函数队列中是空的,调用栈里面就没东西
- 5s到了之后,定时器把cb1函数推到回调函数队列,此时回调函数队列中有函数,事件循环机制把函数拿过来,再推到调用栈中
- 此时调用栈中他是一个函数体(cb1函数),此时再一行行执行代码
console.log('cb1')
- 此时和第一行代码一样,将console放到调用栈中,打印
- 打印完之后,当前那行代码在执行栈被清空
- 当那行代码执行完,方法也被执行完了,整个方法在调用栈中被清空
- 此时所有代码执行完毕
- 图示1 => 最初的状态
- 总结event loop过程
- 同步代码,一行行放在call stack(调用栈)执行
- 遇到异步,会先记录下,等待时机(定时、网络请求等)
- 时机到了,就移动到Callback Queue(回调函数队列)
- 如果call stack(调用栈)为空(即同步代码执行完),Event Loop开始工作
- 轮询查找回调函数队列,如果有则移动到执行栈执行
- 然后继续轮询查找(永动机一样)
- 这样异步就可以永远执行下去
# 3.1.2. DOM事件和event loop
- 示例:
<button id="btn1">提交</button>
<script>
$('#btn1').click(e=> {
console.log('button clicked');
});
console.log('Bye');
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 触发回调的时机不一样(事件=> 点击、鼠标移入等用户交互)
- 页面加载实际上click是立即就执行的(
$('#btn1').click
) - 但里面函数不执行,只是将事件放入web api中
- 其余和上面讲述的相同
- 触发时机(请求,事件,定时器等)是由浏览器控制的
- 页面加载实际上click是立即就执行的(
- DOM事件和event loop关联性很强
- DOM事件是基于event loop实现的
- 注意事项
- JS是单线程的
- 异步(setTimeout,ajax等)使用回调,基于event loop
- DOM事件也使用回调,基于event loop
- DOM事件虽然是基于event loop实现,但它不是异步
- 总结来讲,只要是基于回调,就是基于event loop来实现
# 3.2. promise进阶
# 3.2.1. 三种状态
- pending
- 过程中(还没有结果)
- resolved
- 已解决(成果)
- rejected
- 被拒绝(失败)
- 过程
- pending => resolved或pending => rejected
- 变化不可逆(成功或者失败都回不去了)
- 示例
// pending const p1 = new Promise((resolve, reject)=> { }) console.log('p1', p1); // pending状态 // resolve const p2 = new Promise((resolve, reject)=> { // 模拟异步 setTimeout(()=> { // 在异步中执行成功回调 resolve(); }); }) // 异步中的方法块还没有执行 console.log('p2', p2); // 一开始打印时,pending状态 // 在异步中执行成功回调,状态改变 setTimeout(()=> console.log('p2-setTimeout', p2)) // reject const p3 = new Promise((resolve, reject)=> { // 模拟异步 setTimeout(()=> { // 在异步中执行失败回调 reject(); }); }) // 异步中的方法块还没有执行 console.log('p3', p3); // 一开始打印时,pending状态 // 在异步中执行失败回调,状态改变 setTimeout(()=> console.log('p3-setTimeout', p3))
1
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
29
30
31
# 3.2.2. 状态的变化和表现
- pending状态,不会触发then和catch
- resolved状态,会触发后续的then回调函数
- rejected状态,会触发后续的catch回调函数
- Promise使用技巧
- 直接使用resolve
const p1 = Promise.resolve(100) console.log('p1', p1); // 后面加catch不会触发,因为只有resolve状态 p1.then(data=> { console.log("data", data); })
1
2
3
4
5
6- 直接使用reject
const p2 = Promise.reject('err2') console.log('p2', p2); p2.catch(err=> { console.log("err", err); });
1
2
3
4
5
# 3.2.3. then和catch改变状态
- then正常返回resolved,里面有报错则返回rejected
- catch正常返回resolved,里面有报错则返回rejected
- 代码演示
// then正常返回resolved,里面有报错则返回rejected const p1 = Promise.resolve().then(()=> { return 100 }) console.log("p1", p1); // resolve状态 触发后续then的回调 // 因为p1是resolve状态 p1.then(()=> { console.log('p3 2333'); // 触发后续then的回调 }) const p2 = Promise.resolve().then(()=> { throw new Error('then error p2') }) console.log("p2", p2); // rejected状态 触发后续catch的回调 // 因为p1是rejected状态 p2.then(()=> { console.log('24444'); // 无法触发 }).catch(err=> { console.log('err', err); // 可以触发 }) // catch正常返回resolved,里面有报错则返回rejected const p3 = Promise.reject('my error').catch(err=> { console.log('err', err); }); console.log("p3", p3); // 此时的状态是resolved - 重点! // 因为p3是resolve状态 p3.then(()=> { console.log('p3 2333'); // 触发后续then的回调 }) const p4 = Promise.reject('my error').catch(err=> { throw new Error('then error p4') }); console.log("p4", p4); // 此时的状态是reject // 因为p4是rejected状态 p4.then(()=> { console.log('24444'); // 无法触发 }).catch(err=> { console.log('err', err); // 可以触发 }) // p4.catch()里面没有报错,即它的promise状态也是resolved
1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42 - 总结
- 不管是then还是catch,只要是正常返回,promise状态就都是resolved,只要报错,promise状态就都是rejected
- 面试题解答
// 第一题 // Promise.resolve属于resolved状态,会触发then Promise.resolve().then(() => { console.log(1) // 1 resolve状态的promise不会执行catch }).catch(() => { console.log(2) // 不执行 }).then(() => { console.log(3) }) // 整个promise执行完之后,返回的也还是resolved状态的promise // 1、3 // 第二题 Promise.resolve().then(() => { console.log(1) // 1 throw new Error('erro1') // 报错,那么整个then方法中的promise状态就是rejected,会执行catch回调 }).catch(() => { console.log(2) // 2 无论catch还是then,只要不报错,返回的promise状态就是resolved,执行then回调 }).then(() => { console.log(3) // 3 }) // 整个返回的也是resolved状态的promise // 1、2、3 // 第三题 Promise.resolve().then(() => { // rejected 触发catch回调 console.log(1) // 1 throw new Error('erro1') }).catch(() => { console.log(2) // 2 resolve 触发then回调 }).catch(() => { console.log(3) }) // 1、2
1
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
29
30
31
32
33
# 3.2.4. promise总结
- 三种状态,状态的表现和变化
- pending、resolved、rejected
- 变化
- pending => resolved
- pending => rejected
- 表现
- pending不会触发任何回调
- resolved会触发then回调
- rejected会触发catch回调
- then和catch对状态的影响(重点)
- 无论是then还是catch,只要里面没有报错,返回的就是resolved状态的promise,只要有报错,返回的就是rejected状态的promise
- then和catch的链式调用(常考)
# 3.3. async/await
# 3.3.1. 基本使用
- 背景
- 异步回调有callback hell风险的
- 为了解决这个问题,可以使用promise then catch链式调用
- 但这也是基于回调函数
- async/await是同步语法,彻底消灭回调函数
- 代码演示
function loadImg(src) { const p = new Promise( (resolve, reject) => { const img = document.createElement('img') img.onload = () => { resolve(img) } img.onerror = () => { const err = new Error(`图片加载失败 ${src}`) reject(err) } img.src = src } ) return p } // 如果不写分号,后面有括号,他会当做一个函数执行,所以这里不写分号,下面匿名函数会报错 const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg' const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg' // 匿名函数 // 如果前面不加分号,后面要加!(非),表示和前面隔开 !(async ()=> { // 同步写法,实现异步代码 // 执行await函数必须使用async包裹 const img1 = await loadImg(url1); // loadImg不是async,是promise对象 console.log(img1.height, img1.width) const img2 = await loadImg(url2); console.log(img2.height, img2.width) })() const loadImg1 = async ()=> { const img1 = await loadImg(url1); return img1 } const loadImg2 = async ()=> { const img2 = await loadImg(url2); return img2 } !(async ()=> { // 执行await函数必须使用async包裹 const img1 = await loadImg1(); // loadImg1是async函数,不是promise对象 console.log(img1.height, img1.width) const img2 = await loadImg2(); console.log(img2.height, img2.width) })()
1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 3.3.2. async/await和Promise的关系
- 回顾
- async/await是消灭异步回调的终极武器
- 和Promise并不互斥
- 两者相辅相成(必须结合起来用)
- 关系
- 执行async函数,返回的是Promise对象
- await相当于promise的then
- try...catch可捕获异常,代替了promise的catch
- 代码演示
- 执行async函数,返回的是Promise对象
async function fn1() { // 如果返回一个值,他会内部封装成一个promise对象 // 如果返回promise对象,就按原状返回一个promise对象 // return Promise.resolve(200) return 100 // 相当于return Promise.resolve(100) } const res1 = fn1(); // 执行async函数,返回的是一个promise对象 console.log("res1", res1); // 100 resolved状态 promise对象 // resolved状态的promise对象,可以执行then回调 res1.then(data=> { console.log("data", data) // 100 })
1
2
3
4
5
6
7
8
9
10
11
12 - await相当于promise的then
!(async ()=> { const p1 = Promise.resolve(300) const data = await p1 // await相当于promise的then console.log('data', data); // 300 })() // 如果await下面只是一个值 !(async ()=> { // 如果await后面是promise,他会当做then使用 // 如果不是promise,他会进行内部封装 => await Promise.resolve(400) const data1 = await 400 console.log('data1', data1); // 400 })()
1
2
3
4
5
6
7
8
9
10
11
12
13 - 前面两条规则连起来
async function fn4() { // 返回的都是promise对象 return 200 // return Promise.resolve(200) } !(async ()=> { // await相当于promise的then const data2 = await fn4() console.log('data2', data2); // 200 })()
1
2
3
4
5
6
7
8
9
10
11 - try...catch可捕获异常,代替了promise的catch
!(async ()=> { const p4 = Promise.reject('err') // reject状态的promise // promise如果是reject状态,就可以使用try catch捕获 try { const res = await p4 console.log("p4", res) } catch(ex) { console.error(ex); // try...catch相当于promise的catch } })()
1
2
3
4
5
6
7
8
9
10
11 - await、then、try...catch
- 面试很可能会考
!(async ()=> { const p5 = Promise.reject('err') // reject状态的promise const res = await p5 // 他是reject状态,不会走then,只会走try catch // 下面不会执行 console.log("p5", res) })()
1
2
3
4
5
6
- 执行async函数,返回的是Promise对象
- 总结
- async函数他是封装promise的,返回的也是promise
- await相当于promise的then,处理promise成功的情况
- 在async函数中,promise状态为失败的情况,需要用try...catch捕获
# 3.4. 异步的本质
- 回顾
- async/await是消灭异步回调的终极武器
- JS还是单线程,还是得有异步,还得是基于event loop
- async/await只是一个语法糖,但这颗糖真香
- 代码演示
- 示例1
const async1 = async ()=> { console.log("async1 start"); // 立马执行 重要 // 它返回的实际上是undefined,await执行不执行其实没有意义 await async2() // 先执行async2,再执行await这个操作 // 执行await之后,下面的都是异步 // await的后面,都可以看作是callback里面的内容,即异步 // await下面的方法需要放到callback里面去等待执行 // 类似event loop,setTimeout(cb1) // 也可能是Promise.resolve().then(()=> {console.log('async1 end')}) // 现在可以把上面两个当作是一个东西,涉及到微任务/宏任务之后进行细分 console.log('async1 end'); } const async2 = async ()=> { console.log("async2"); // 立马执行 重要 } // script start // async1 start // async2 // !!! 因为await下面是异步 // script end // 此时同步代码已经执行完,启动事件循环机制,在回调函数的队列里面去找异步的代码去执行 // async1 end // 执行异步代码 console.log('script start'); async1(); // 执行该方法,里面的代码立马执行(还没到异步) console.log('script end');
1
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 - 示例2
const async3 = async ()=> { console.log("async3 start"); // 2 await async4(); // 下面三行都是异步回调,即callback内容 // 他们被放到异步队列里面 console.log('async3 end'); // 5 await async5(); // 下面一行是异步回调的内容 // 实际上这是层级嵌套的关系 console.log('async3 end 2'); // 7 } const async4 = async ()=> { console.log("async4"); // 3 } const async5 = async ()=> { console.log("async5"); // 6 } // script2 start // async3 start // async4 // script2 end // async3 end // async5 // async3 end 2 console.log('script2 start'); // 1 async3(); console.log('script2 end'); // 4 // 此时同步代码执行完,根据事件循环机制,开始执行异步队列中的代码
1
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
29
30
31
32
33
- 示例1
- 总结
- async/await是用同步的代码去写异步
- 但他改变不了异步的本质
- await后面的东西,都可以看作是callback里面的内容,即await下面的代码都会被放到异步队列里面
- 它实际执行的方法和callback是一样的,即也和异步是一样的
- await下面如果还有await,实际是嵌套关系
- 同步代码执行完之后,根据事件循环机制,再从异步队列里面拿出方法,放入调用栈去执行
# 3.5. for...of
- 概念
- for...in(以及forEach、for)是常规的同步遍历
- for...of常用于异步的遍历
- 代码演示
// 模拟异步 const muti = num=> { return new Promise(resolve=> { setTimeout(()=> { resolve(num * num); }, 1000); }) } const nums = [1, 2, 3] // 同步循环 // 因为要用异步语法,所有必须用async函数 // 同步的代码不会等待,会一遍一遍往下执行 // 他会一下子把数组遍历完,然后一下子把数组中的值进行计算,最后再一下子打印出结果 // 一瞬间执行了三遍,所以一秒钟之后同时打印出来 nums.forEach(async i=> { // 因为muti是异步的,这里使用await实现异步 let res = await muti(i); // 1s之后计算的结果同时打印出来 console.log(res) }) // 如何一个一个数字进行打印呢? // 异步循环 !(async ()=> { for(let i of nums) { // 先执行第一个 // 第一个有了结果之后执行第二个 // 以此类推 let res = await muti(i); console.log(res) } })()
1
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
29
30
31
32
33
34
# 3.6. async/await总结
- async/await解决了异步回调,是一个很香的语法糖
- async/await和promise的关系,很重要
- 执行async函数,返回的是Promise对象
- await相当于promise的then
- try...catch可捕获异常,代替了promise的catch
- for...of的使用
- 是一种异步循环
# 3.7. 微任务/宏任务
# 3.7.1. 概念
- 简述
- 宏任务 => macroTask
- 微任务 => microTask
- 知识点
- 什么是宏任务,什么是微任务
- event loop和DOM渲染
- 只有从这里面才能体现出宏任务和微任务的区别
- 微任务和宏任务的区别
- 代码演示
console.log(100); // 1 // 宏任务 setTimeout(()=> { console.log(200); // 3 }) /* setTimeout(()=> { console.log(201); // 4 同类型的异步和出场顺序有关 }) */ // 微任务 Promise.resolve().then(()=> { console.log(300) // 3 不同类型的异步和出场顺序无关 }) console.log(400) // 2 同步代码执行完毕 // 100 400 300 200 201
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 - 宏任务和微任务的区别
- 宏任务和微任务其实就相当于所有的异步(DOM事件除外)
- DOM事件也是基于事件循环机制来执行的
- 宏任务:setTimeout、setInterval、Ajax、DOM事件
- 微任务:Promise、async/await
- 微任务执行时机比宏任务要早(先记住)
- 宏任务和微任务其实就相当于所有的异步(DOM事件除外)
# 3.7.2. event loop和DOM渲染
- 问题
- 为什么微任务执行时机比宏任务早
- 简述
- 需要再次回归一遍event loop的过程
- JS是单线程的,而且和DOM渲染共用一个线程(资源)
- JS执行的时候,得留一些时机供DOM渲染
- js一遍一遍执行,浏览器界面不能一直不变
- 回顾event loop过程(增加DOM渲染时机)
- 参考代码
console.log('Hi') setTimeout(function cb1() { console.log('cb1') // cb即callback }, 5000) console.log('Bye');
1
2
3
4
5
6
7 - 名称
- Browser console => 浏览器console环境
- Call Stack => 调用栈
- Web APIs => 浏览器定义的api(DOM/BOM)
- Callback Queue => 回调函数队列
- Event Loop => 事件循环机制
- 执行过程
- 执行第一行代码,把它推到调用栈,打印,移出调用栈
- 执行到定时器
- 把setTimeout cb1放到调用栈
- 把定时器里面的方法放到Web API中
- 放到Web API中定时器就执行完了
- 移出调用栈
- 继续往下执行
- 同步代码继续推到调用栈,打印,移出调用栈
- 最后调用栈清空,没有任何同步代码推到调用栈执行的时候
- 第一次轮询
- 启动事件循环机制
- 一遍一遍地遍历回调函数队列
- 如果异步回调队列有值,就会将它推到调用栈中执行
- 最后还漏了一个步骤
- 在调用栈空闲(没有同步代码推入调用栈)
- 先尝试DOM渲染
- 再去触发事件循环机制
- 将定时器放入调用栈,执行完毕之后移出调用栈
- 此时调用栈空闲
- 会再次尝试DOM渲染
- 再去触发事件循环机制
- 第二次轮询
- 补充说明
- 每次调用栈清空(即每次轮询结束),即同步任务执行完
- 包括异步代码推到调用栈执行结束
- 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
- 然后再去触发下一次事件循环
- 每次调用栈清空(即每次轮询结束),即同步任务执行完
- 参考代码
- 代码示例
- 体会dom渲染和js执行的关系
// event loop和DOM渲染 // 执行完js代码之后,还需要渲染到页面上 const $p1 = $('<p>一段文字</p>') const $p2 = $('<p>一段文字</p>') const $p3 = $('<p>一段文字</p>') $('#container').append($p1) .append($p2) .append($p3) console.log("length", $('#container').children().length) // alert会阻断js执行,也会阻断DOM渲染,便于查看效果 // alert之前,js已经执行完成了,但DOM还未渲染 alert('本次调用栈结束,DOM结构已经更新,但尚未触发渲染')
1
2
3
4
5
6
7
8
9
10
11
12
13
14 - 微任务和宏任务的区别
- 宏任务:DOM渲染后触发,如setTimeout
- 微任务:DOM渲染前触发,如Promise
- 微任务 => DOM渲染 => 宏任务
- 为什么微任务和宏任务执行顺序是这样的
- 代码示例
// event loop和DOM渲染 // 执行完js代码之后,还需要渲染到页面上 const $p1 = $('<p>一段文字</p>') const $p2 = $('<p>一段文字</p>') const $p3 = $('<p>一段文字</p>') $('#container').append($p1) .append($p2) .append($p3) // 微任务:DOM 渲染前触发 Promise.resolve().then(()=> { console.log("length1", $('#container').children().length) alert('Promise then') // 此时dom未渲染 }) // 宏任务:DOM 渲染后触发 setTimeout(()=> { console.log("length2", $('#container').children().length) alert('Promise setTimeout') // 此时dom渲染了 })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 - 原理
- 角度
- 从事件循环解释,为何微任务执行更早
- 即为何微任务在dom前触发
- setTimeout执行机制 => 宏任务
- setTimeout实际上就是把callback放到定时器中
- 定时器等待一个时机 => 定时时间结束
- 放入异步回调队列中
- 由事件循环机制,将异步队列中的方法放到调用栈中
- 当调用栈空闲(每次轮询结束)的时候,会尝试DOM渲染
- 尝试DOM渲染之后继续触发事件循环
- Promise机制 => 微任务
- 执行promise的时候,他会放到micro task queue里面等待时机
- 他不会经过web apis
- 因为promise是ES规范,不是w3c规范
- 他和宏任务队列(callback queue)是分开的
- 为什么
- 微任务是ES6语法规定的
- 宏任务是由浏览器规定的
- 完善事件循环机制
- 同步代码执行完之后,即调用栈清空
- 执行当前的微任务(微任务队列)
- 尝试DOM渲染
- 触发事件循环机制
- 最后执行宏任务
- 图示
- 角度
- 总结(面试题)
- 宏任务有哪些?微任务有哪些?为什么微任务触发时机更早
- 宏任务、微任务和DOM渲染的关系
- 微任务、宏任务和DOM渲染,在event loop的过程
- 代码示例
# 4. 回顾面试题
- 回顾event loop的过程
- 和DOM渲染的关系
- 微任务和宏任务在event loop过程中的不同处理
- 什么是宏任务和微任务,两者区别
- 宏任务:setTimeout、setInterval、Ajax、DOM
- 微任务:Promise、async/await
- 微任务执行时机比宏任务要早
- 微任务在DOM渲染前触发,宏任务在DOM渲染后触发
- Promise的三种状态
- pending、resolved、rejected
- pending -> resolved 或 pending -> rejected
- 变化不可逆
- Promise then和catcha的连接
- then后面是resolved状态
- resolved状态后面跟then可以打印
- 抛出异常后面是rejected状态
- rejected状态后面跟catch可以打印
- async/await和promise关系
- async返回的永远是promise
- await相当于then
- 代码示例
// 示例一 async function fn() { return 100 } (async function() { const a = fn() // ? const b = await fn() // ? console.log(a); // promise console.log(b); // 100 })() // 示例二 (async function() { console.log('start'); const a = await 100 // 直接返回 console.log('a', a) // await相当于then,resolved后面加then可以获取值,即直接得到Promise里面的返回值(200) const b = await Promise.resolve(200) console.log('b', b) // await相当于then,rejected后面只能用catch const c = await Promise.reject(300) // 后面的方法属于回调,上面返回的是rejected,下面没有try catch console.log('c', c) console.log('end') })() // start 100 200
1
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
- promise和setTimeout的顺序
- setTimeout属于宏任务,promise属于微任务
- 微任务先于宏任务执行
- async/await的顺序问题
// script start // async1 start // async2 // promise1 // script end // async1 end // promise2 // setTimeout async function async1() { console.log('async1 start') // 2 同步 await async2() // 先执行async2,再执行await // 执行await,await后面的都是回调 - 微任务 // 同步代码执行过程中,微任务会被放到micro task queue里面等待时机 console.log('async1 end') // 6 第一个微任务 } async function async2 () { console.log('async2') // 3 同步 } console.log('script start') // 1 同步 // 宏任务 // 同步代码执行过程中,宏任务会被放到Callback Queue里面等待时机 setTimeout(function() { console.log('setTimeout'); // 8 最后执行宏任务 }, 0) async1() // 初始化promise时,传入的函数会立刻被执行 new Promise(function(resolve) { console.log('promise1') // 4 同步 resolve() // 变成了一个resolved状态下的promise // 即then会被触发 // 这里的then是异步,是一个微任务 }).then(function() { console.log('promise2') // 7 第二个微任务 }) console.log('script end'); // 5 同步 -> 同步代码执行完毕 => 调用者(call stack)被清空 // 同步代码执行完毕之后 => 微任务队列执行 => 尝试触发渲染DOM => 触发事件循环机制 => 宏任务队列执行
1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 5. 回顾知识点
- 简述
- event loop
- promise进阶
- async/await
- 微任务/宏任务
- event loop过程1
- 同步代码,一行一行放在call stack执行
- 遇到异步,会先'记录'下,等待时机(定时、网络请求等)
- 时机到了,就移动到callback queue
- event loop过程2
- 如果call stack为空(即同步代码执行完),event loop开始工作
- 轮询查找callback queue,如有则移动到call stack执行
- 然后继续轮询查找(永动机一样)
- DOM事件和event loop
- JS是单线程的
- 异步(setTimeout、ajax等)使用回调,基于event loop
- DOM事件也使用回调,基于event loop
- DOM事件和异步,他们触发的时机不一样,但本质都是事件循环
- Promise进阶
- 三种状态,状态的表现和变化
- then和catch对状态的影响(重要)
- then和catch的链式调用(常考)
- async/await
- async/await解决了异步回调,我们可以使用同步代码来写异步
- 是一个语法糖,其本质还是promise
- async/await和Promise的关系,非常重要
- 执行async函数,返回的是Promise对象
- await相当于promise的then
- try...catch可捕获异常,代替了promise的catch
- for...of的使用
- 微任务/宏任务
- 宏任务有哪些?微任务有哪些?为什么微任务触发时机更早
- 微任务是ES6语法规定的
- Promise、async/await
- 宏任务是由浏览器规定的
- setTimeout、setInterval、Ajax、DOM事件
- 微任务是ES6语法规定的
- 宏任务、微任务和DOM渲染的关系
- 微任务 => DOM渲染 => 宏任务
- 微任务、宏任务和DOM渲染,在event loop的过程
- 微任务会被放到micro task queue里面等待时机
- 宏任务会被放到Callback Queue里面等待时机
- 调用栈被清空之后(同步代码执行完)
- 先执行微任务(不会走web api,直接执行)
- 微任务被一个个拖到调用栈
- 微任务执行完毕之后,尝试DOM渲染
- 尝试DOM渲染之后,触发event loop(事件轮询机制)(永动机)
- 执行宏任务
- 宏任务被一个个拖到调用栈
- 执行宏任务
- 宏任务有哪些?微任务有哪些?为什么微任务触发时机更早
~End~