浏览器原理面试题 前端「前端面试进阶之浏览器原理」(18)


当开始执行 JS 代码时,根据先进后出的原则 , 后执行的函数会先弹出栈 , 可以看到,foo 函数后执行,当执行完毕后就从栈中弹出了 。
平时在开发中,可以在报错中找到执行栈的痕迹:
function foo() {
throw new Error('error')
}
function bar() {
foo()
}
bar()
img
可以看到报错在 foo 函数 , foo 函数又是在 bar 函数中调用的 。当使用递归时,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话 , 就会出现爆栈的问题
function bar() {
bar()
}
bar()
img
9. Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行顺序?
Node 中的 Event Loop 和浏览器中的是完全不相同的东西 。
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行 。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行 。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段 。
image
(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始 。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务 , 执行完微任务后再进入 Pending callbacks 阶段 。
(2)Pending callbacks:执行推迟到下一个循环迭代的I / O回调(系统调用相关的回调) 。
(3)Idle/Prepare:仅供内部使用 。
(4)Poll(轮询阶段):
当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同 , 不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后 , 就执行相应微任务 。执行完所有的回调后,变为下面的情况 。
当回调队列为空时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行 , 会结束轮询阶段,进入 Check 阶段 。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调 , 直到所有回调执行完毕 。
(5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务 , 执行完微任务后再进入 Close callbacks 阶段 。
(6)Close callbacks:执行一些关闭回调,比如socket.on('close', ...)等 。
下面来看一个例子 , 首先在有些情况下,定时器的执行顺序其实是随机的
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
对于以上代码来说 , setTimeout 可能执行在前 , 也可能执行在后
首先 setTimeout(fn, 0) === setTimeout(fn, 1) , 这是由源码决定的
进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间 , 那么在 timer 阶段就会直接执行 setTimeout 回调
那么如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了
当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
在上述代码中,setImmediate 永远先执行 。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行 , 当回调执行完毕后队列为空,发现存在 setImmediate 回调 , 所以就直接跳转到 check 阶段去执行回调了 。