目录
前言
本次文章默认阅读者已掌握JS的单线程和异步的特性,以及事件循环机制的基本原理。内容如有错误欢迎指出。
解题秘诀
来来来,跟着我的经验总结后,保证你掌握!先要知道微任务和宏任务大致有哪些。
微任务记两个
面试题一般只会出到第一和第二个,记住这两个就行。
Promise.then()
await xxx
下一行的执行代码MutationObserver
Object.observe
process.nextTick
- …
宏任务(不用记)
看起来很多,别怕,你就把微任务以外的都作为宏任务看待,因为面试题也不会出这个圈。
setTimeout
setInterval
setImmediate
MessageChannel
requestAnimationFrame
I/O
UI交互事件
- …
知识拓展
UI交互事件:例如DOM事件,举例:给一个按钮标签绑定一个点击事件,那么执行到这个函数定义时,会把点击事件里的函数放进web api中,等到用户点击了,才放入宏任务队列中。(注意,并不是异步调用)
定时器任务:执行到定时器时,会把计时这件事交给计时线程去做,到时间了,才把回调推给宏任务队列里。
记个Promise的同步部分
举例:
new Promise((resolve, reject)=>{ //new一个Promise对象是直接执行的
console.log(1) // A
resolve()
console.log(2) // B
// 这块都可以看作是同步执行的,也就是执行了A和B,C是微任务最后执行。
}).then(()=>{
console.log(3) // C
})
// 结果为1 2 3
再记个Async的同步部分
还是举例:
async function async1() {
console.log(1) //同步执行
await fn() //同步执行
console.log(2) //这就是回调,放入微任务
}
好了就需要这么点,就可以解决大部分基础的面试题了。
面试题
再次提示:我们解决的主要是基础的面试题哦,比较复杂的不在本次文章的范围内
例子一
// 代码执行的结果?
const promise1 = new Promise((resolve, reject)=>{
console.log(1)
resolve()
console.log(2)
})
setTimeout(()=>{console.log(3)},0)
promise1.then(()=>{
console.log(4)
})
console.log(5)
解答:
主线程从上到下执行
第一轮:
1 setTimeout是直接触发的,所以先压入执行栈执行,调用Web api。内部内容属于宏任务,把console.log(3)弹出推入事件队列中,setTimeout离开执行栈。
2 到promise1函数,压入执行栈中执行,先执行同步部分,先压入console.log(1)后弹出执行栈,压入console.log(2)后弹出执行栈,打印出1,2。then()里的console.log(4)弹出推送到微任务队列中。promise1离开执行栈。
3 到console.log(5),压入执行栈,弹出直接执行,打印5。
4 看看微任务队列有什么,有个promise1的then()回调,console.log(4)压入并弹出执行栈,打印出4。
5 看看事件队列有什么,有个setTimeout的回调,压入执行栈,进入第二轮循环。
第二轮:
1 setTimeout的回调,也就是console.log(3),弹出执行,打印3 。
所以结果是1,2,5,4,3
例子二
function fn(){
console.log(1);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve,reject){
console.log(3);
resolve();
// reject()
}).then(function(){
console.log(4);
},function(){
console.log(5)
});
console.log(6);
}
fn()
解答:
第一轮:
1 执行fn函数,压入执行栈,然后看内部代码,先是console.log(1)压入到fn上面,弹出后打印1 。(fn内部的执行代码都会压在执行栈中都会压在fn上)
2 执行setTimeout,…console.log(2)推入事件队列。
3 执行Promise,…弹出console.log(3)打印3 。没有执行错误所以把console.log(4)推入微任务队列。
4 执行console.log(6),弹出console.log(6),打印6,fn()出栈 。
5 微任务队列,console.log(4)压入执行栈并弹出执行,打印出4 。
6 事件队列,setTimeout的console.log(2)压入执行栈,开始第二轮。
第二轮:
1 console.log(2)弹出执行,打印2。
结果为:1,3,6,4,2
例子三
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')
解答:这次就说的简洁一些了
第一轮:
1 执行console.log(‘script start’) 打印script start
2 setTimeout中console.log(‘setTimeOut’)进入事件队列
3 async1中先执行console.log(‘async1 start’),打印async1 start ,然后await执行async2函数,执行console.log(‘async2’),打印async2 ,console.log(‘async1 end’)进入微任务队列
4 Promise执行console.log(‘promise1’),打印promise1 ,没错误,console.log(‘promise2’)进入微任务队列
5 执行console.log(‘script end’),打印script end
6 看微任务队列,执行async1的console.log(‘async1 end’),再执行Promise的console.log(‘promise2’),打印出async1 end和promise2
7 看事件队列,setTimeout的console.log(‘setTimeOut’)进入执行栈,进入下一轮
第二轮:
1 执行console.log(‘setTimeOut’),打印setTimeOut
结果为:script start,async1 start,async2,promise1,script end,async1 end,promise2,setTimeOut
例题四
const promise1 = new Promise((resolve, reject) => {
console.log('1')
setTimeout(() => {
resolve('2')
}, 0)
})
promise1.then(res => {
console.log(res);
setTimeout(() => {
console.log('3');
}, 0)
})
setTimeout(() => {
console.log('4');
}, 0)
console.log('5');
这题会让你稍微犹豫的就是各种定时器的使用,别怕,万变不离其宗。
例题五
function a() {
log(1)
Promise.resolve().then(function(){
log(2)
})
}
setTimeout(function(){
log(3)
}, 0)
Promise.resolve().then(a)
log(5)
DOM渲染在宏任务前,微任务后执行
解释
这里先解释下为什么JS设计成单线程的原因之一,其实也只要记住一个就可以了。因为JS能够控制DOM,如果DOM的渲染是单独一个线程,那当DOM的渲染过程中JS操作了DOM,就发生冲突了。所以JS和DOM渲染必须共用一个线程,二者其中一个执行的时候,另外一个只能停止。
那么既然同一个线程,DOM的渲染在事件循环中的哪个阶段呢?
在页面加载完成后,DOM的渲染动作一般会在事件循环中执行栈清空后,启动下个事件循环之前,也就是夹在这两者之间。那么也就是说,DOM渲染在宏任务前,微任务后执行。
为什么会分宏任务和微任务,是因为微任务是属于es规范的,而宏任务是属于w3c规范的(浏览器),将二者区分开,且本轮微任务的执行在下一轮宏任务之前。
例子一
来个DOM渲染有关的题目
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
Promise.resolve().then(() => {
const length = $('#container').children().length
alert(`micro task ${length}`)
})
setTimeout(() => {
const length = $('#container').children().length
alert(`macro task ${length}`)
})
// 问两次alert时,页面的渲染情况
解答:
- 首先Promise推入微任务到微任务队列中,然后setTimeout推入宏任务到宏任务对列中。
- DOM的渲染在微任务之后,下个事件循环之前(宏任务被执行之前),所以微任务的alert执行时,页面没更新,之后setTimeout被执行,alert执行时,页面已经更新。
知识更新
其实不知在某个时间节点上,浏览器已经把宏任务队列这种机制去掉了。取而代之的是更加多样的任务队列。因为页面越来越复杂了,单单一个宏任务已经渐渐力不从心了。
例如有交互触发队列(优先级高)、定时器回调任务队列(优先级中)等等,目前就只关心这俩个即可。
你可以理解为宏任务队列被拆成不同执行优先级的队列了。
目前大多数面试官都不知道这个更新的东西,了解即可。