浏览器事件循环
- 浏览器宿主环境中包含5个线程:
- JS引擎:负责执行执行栈的最顶部代码
- GUI线程:负责渲染页面
- 事件监听线程:负责监听各种事件
- 计时线程:负责计时
- 网络线程:负责网络通信
(js引擎和gui线程会相互等待)
当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入事件队列。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。
- 只有在执行栈中,才会被执行
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:
- 宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
- 微任务(队列):MutationObserver,Promise产生的回调进入微队列
举例:
console.log('script start')
setTimeout(()=>{
console.log('宏队列');
}, 1000)
Promise.resolve().then(()=>{
console.log('微队列');
})
- 代码执行,入栈全局上下文,入栈console.log()上下文,控制台输出'script start',console.log()上下文出栈。
- setTimeout()执行,通知计时线程计时,Promise.resolve执行,then回调函数加入微队列,全局上下文出栈。此时执行栈空,检查微队列,把微队列第一个函数拿到执行栈执行,输出'微队列'。此时执行栈空,等待。
- 1s后计时结束,setTimeout的回调函数加入到宏队列。因为执行栈此时空闲,检查微队列没东西,所以把宏队列第一个函数拿到执行栈执行,输出'宏队列'。程序运行结束。
node事件循环
node环境,有很多队列,主要有以下三个,事件循环时,会挨个检查。
- timers: 存放计时器的回调函数
- poll: 轮询队列,存放大部分回调,比如文件读取,监听用户请求。
poll中有回调,依次执行,直到清空队列;poll没有回调,轮询等待其他队列出现回调,然后进入下一阶段 - check: setImmediate回调函数
- 执行一个回调之前,必须清空nextTick和promise队列
举例:
console.log('main');
setTimeout(()=>{
console.log('timers')
}, 3000)
setImmediate(()=>{
console.log('check')
})
主程序执行,打印"main",然后开始事件循环。
检查timers队列,没有回调,进入下一阶段
检查poll队列,没有回调,别的队列有回调,进入下一阶段
检查check队列,输出"check"
第二圈:
检查timers队列,没有回调,进入下一阶段
检查poll队列,没有回调,等其他队列出现回调。3s时间到,timers队列出现回调函数,进入下一阶段。
检查check队列,没有回调
第三圈:
检查timers队列,输出"timers"
检查poll队列,没有回调,并且没有任何值得等待的东西了(没有监听用户请求,没有计时器等等),进入下一阶段
检查check队列,没有回调
事件循环结束!
除了上面过程,再补充两点
- nextTick和promise不属于事件循环,但是执行任何回调前,必须先清空nextTick再清空promise
- 监听某个端口时,程序不会结束,是卡在了poll,它会一直等,所以事件循环不会结束
扩展:实际上timers不是一个队列,而是一个计时器一个计时器的查看它时间到没到(有运算,所以运行会慢),时间到了就把回调拉过来。而check可以当成一个数组,里面的回调直接运行。所以 setImmediate 运行效率比 setTimeout高 (当然开定时器的过程也会有一定的时间消耗)。
下面程序可以对比setTimeout 和 setImmediate 运行效率
let i = 0;
console.time()
function test(){
i ++;
if(i < 1000){
setTimeout(test, 0)
// setImmediate(test)
} else {
console.timeEnd();
}
}
test(); // 1583.377ms // 6.946ms