【JS基础】克服事件循环机制的基础面试题,一点也不难

前言

本次文章默认阅读者已掌握JS的单线程异步的特性,以及事件循环机制的基本原理。内容如有错误欢迎指出。


解题秘诀

来来来,跟着我的经验总结后,保证你掌握!先要知道微任务和宏任务大致有哪些。

微任务记两个

面试题一般只会出到第一和第二个,记住这两个就行。

  1. Promise.then()
  2. await xxx 下一行的执行代码
  3. MutationObserver
  4. Object.observe
  5. process.nextTick

宏任务(不用记)

看起来很多,别怕,你就把微任务以外的都作为宏任务看待,因为面试题也不会出这个圈。

  1. setTimeout
  2. setInterval
  3. setImmediate
  4. MessageChannel
  5. requestAnimationFrame
  6. I/O
  7. 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 endpromise2

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时,页面的渲染情况

解答:

  1. 首先Promise推入微任务到微任务队列中,然后setTimeout推入宏任务到宏任务对列中。
  2. DOM的渲染在微任务之后,下个事件循环之前(宏任务被执行之前),所以微任务的alert执行时,页面没更新,之后setTimeout被执行,alert执行时,页面已经更新。

知识更新

其实不知在某个时间节点上,浏览器已经把宏任务队列这种机制去掉了。取而代之的是更加多样的任务队列。因为页面越来越复杂了,单单一个宏任务已经渐渐力不从心了。

例如有交互触发队列(优先级高)、定时器回调任务队列(优先级中)等等,目前就只关心这俩个即可。

你可以理解为宏任务队列被拆成不同执行优先级的队列了。

目前大多数面试官都不知道这个更新的东西,了解即可。

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值