前端面试知识整理——JS异步进阶
主要内容:
- event loop
- promise进阶
- async/await
- 微任务/宏任务
题目:
问答题:
- 请描述event loop(事件循环/事件轮询)的机制,可画图
- 什么是宏任务和微任务,有什么区别
- promise 有哪三种状态?如何变化?
场景题1:promise then和catch的连接
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 3
// 第二题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 2 3
// 第三题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).catch(() => { // 注意这里是 catch
console.log(3)
})
// 1 2
场景题2:async/await语法
async function fn() {
return 100
}
(async function () {
const a = fn() // ?? // promise
const b = await fn() // ?? // 100
})()
;(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')
})()
// start a 100 b 200
场景题3:promise和setTimeout的顺序
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
场景题4:外加async/await的顺序问题
async function async1() {
console.log('async1 start') //2
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务) //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) {
// 返回 Promise 之后,即同步执行完成,then 是异步代码
console.log('promise1') // Promise 的函数体会立刻执行 //4
resolve()
}).then(function () {
// 异步,微任务
console.log('promise2') //7
})
console.log('script end') //5
// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务
1.event loop(事件循环/事件轮询)
- JS是单线程运行的
- 异步要基于回调来实现
- event loop就是异步回调的实现原理
JS如何执行:
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,在执行异步
console.log('hi')
setTimeout(function cb1() {
console.log('cb1')
}, 1000)
console.log('bye')
总结 event loop过程
- 同步代码,一行一行放在call stack中执行
- 遇到异步,会先记录下,等待时机(定时,网络请求等)
- 时机到了,就移动到callback Queue
- 如果call stack为空(即同步代码执行完),event loop开始工作
- 轮询查找callback Queue,如有则移动到call stack中执行
- 然后继续轮询查找(永动机一样)
2.DOM事件和event loop
- JS是单线程的
- 异步(AJAX,setTimeout等),使用回调,基于event loop
- DOM事件也使用回调,基于event loop
3.Promise
- 三种状态
- 状态的表现和变化
- then和catch对状态的影响
三种状态
- pending resolved rejected
- pending->resolved 或 pengding->rejected
- 变化不可逆
const p1 = new Promise((resolve, reject) => {})
console.log(p1) //Promise {<pending>}
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
})
console.log('p2', p2) //pengding 一开始打印
setTimeout(() => {
console.log('p2-settimeout', p2) //fulfilled
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
console.log('p3', p3) //pengding 一开始打印
setTimeout(() => {
console.log('p3-settimeout', p3) //rejected
})
状态的表现
- pengding状态,不会触发then和catch
- resolved状态,会触发后续的then回调函数
- rejected状态,会触发后续的catch回调函数
const p1 = Promise.resolve(100)
p1.then((data) => {
console.log('data1', data) //data1 100
}).catch((err) => {
console.log(err)
})
const p2 = Promise.reject('err')
p2.then((data) => {
console.log('data2', data)
}).catch((err) => {
console.log('err2', err) //err2 err
})
then 和 catch改变状态
- then 正常返回resolved,里面有报错则返回rejected
- catch 正常返回resolved,里面有报错则返回rejected
// then 正常返回resolved,里面有报错则返回rejected
const p1 = Promise.resolve().then(() => {
return 100
})
console.log(p1) //fulfilled
//resolved 后续触发then回调
p1.then(() => {
console.log(123) //123
})
const p2 = Promise.resolve().then(() => {
throw new Error('then error')
})
console.log(p2) //rejected
//rejected后续触发catch回调
p2.then(() => {
console.log(123)
}).catch((err) => {
console.log('catch error', err) //catch error Error: then error
})
// catch 正常返回resolved,里面有报错则返回rejected
const p3 = Promise.reject('my error').catch((err) => {
console.log(err) //my error
})
console.log(p3) //fulfilled
p3.then(() => {
console.log(100) //100
})
const p4 = Promise.reject('my error2').catch((err) => {
throw new Error('error')
})
console.log(p4) //rejected
p4.then(() => {
console.log(200) //不执行
}).catch((err) => {
console.log('p4 error', err) //p4 error Error: error
})
Promise总结
- 三中状态
- then和catch对状态的影响(重要)
- then和catch的链式调用(常考)
4.async/await
- 异步回调 callback hell
- Promise then catch 链式调用,但也是基于回调函数
- async/await是同步语法,彻底消灭回调函数
//async await
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
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
}
;(async function () {
const img1 = await loadImg(src1)
console.log(img1.width)
const img2 = await loadImg(src2)
console.log(img2.width)
})()
async/await和Promise的关系
- async/await是消灭异步回调的终极武器
- 但和Promise并不互斥
- 反而,两者相辅相成
- 执行async函数,返回的是Promise对象
- await相当于Promise的then
- try…catch…可捕获异常,代替了Promise的catch
//执行async函数,返回的是Promise对象
async function fn1() {
return 100
}
const res1 = fn1() //执行async函数,返回的是一个promise对象
console.log('res1', res1) //res1 Promise {<fulfilled>: 100}
res1.then((data) => {
console.log(data) //100
})
// await相当于Promise的then
;(async function () {
const p1 = Promise.resolve(100)
const data = await p1
console.log(data) //100
})()
;(async function () {
const data2 = await 200 //相当于await Promise.resolve(200)
console.log(data2) //200
})()
// try...catch...可捕获异常,代替了Promise的catch
;(async function () {
const p3 = Promise.reject('my error')
try {
const data = await p3
console.log(data) //不执行
} catch (error) {
console.log(error) //my error
}
})()
// try...catch...可捕获异常,代替了Promise的catch
;(async function () {
const p4 = Promise.reject('error')
const data = await p4
console.log(data) //不会执行
})()
异步的本质
- async/await是消灭异步回调的终极武器
- JS还是单线程,还得是有异步,还得是基于event loop
- async/await只是一个语法糖
async function async1() {
console.log('async1 start') //2
//await 后面的内容,都可以看做callback里的内容,即异步
//4同步代码执行完了才执行5 的异步代码
await async2()
console.log('async1 end') //5 关键在这一步,它相当于放在 callback 中,最后执行
}
async function async2() {
console.log('async2') //3
}
console.log('script start') //1
async1()
console.log('script end') //4
async function async1() {
console.log('async1 start') //2
await async2()
console.log('async1 end 1') // 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
for … of
- for … in (以及forEach for)是常规的同步遍历
- for … of常用于异步的遍历
function multi(i) {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(i * i)
}, 1000)
})
return p
}
const nums = [1, 2, 3]
nums.forEach(async (i) => {
const res = await multi(i)
console.log(res) //1秒后,1,4,9同时大打印出来
})
;(async function () {
for (let k of nums) {
const res = await multi(k)
console.log(res)
}
})() //1,4,9间隔一秒分别打印
5.宏任务macroTask和微任务microTask
- 什么是宏任务,什么是微任务
- eventloop 和 DOM 渲染
- 微任务和宏任务的区别
微任务和宏任务
- 宏任务:setTimeout,setInterval,AJAX,DOM事件
- 微任务:Promise async/await
- 微任务执行时机比宏任务要早
console.log(100) //1
// 宏任务
setTimeout(() => {
console.log(200) //4
})
// 微任务
Promise.resolve('100').then(() => {
console.log(300) //3
})
console.log(400) //2
event loop和DOM渲染
- 再次回归一遍event loop的过程
- JS是单线程的,而且和DOM渲染共用一个线程
- JS执行的时候,得留一些时机供DOM渲染
加入DOM渲染的event loop
- 每次Call Stack清空(即每次轮询结束),即同步任务完成
- 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
- 然后再去触发下一次event loop
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('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
微任务和宏任务的区别
- 宏任务:DOM渲染结束后触发,如setTimeout
- 微任务:DOM渲染前触发,如Promise
// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
// 微任务:渲染之前执行(DOM 结构已更新)
Promise.resolve().then(() => {
const length = $('#container').children().length
alert(`micro task ${length}`)
})
// 宏任务:渲染之后执行(DOM 结构已更新)
setTimeout(() => {
const length = $('#container').children().length
alert(`macro task ${length}`)
})
从event loop解释,为何微任务执行更早
- 微任务是ES6语法规范的
- 宏任务是由浏览器规定的
- 微任务会放在单独的micro task queue中
完整event loop执行过程:
- call stack清空
- 执行当前的微任务
- 尝试DOM渲染
- 触发event loop