金秋飒爽,丹桂飘香,我们又迎来了一年一度金九银十招聘季,有一个问题也是经常要cue到,逢人就要问:你知道EventLoop吗,展开说说?
简介
Event Loop ,“事件循环”
浏览器 or Node的一种解决JavaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
为什么要弄懂Event Loop?
- 增加技术深度,懂得JS运行机制
- 底层原理掌握好,万变不离其宗
- 面试官最爱
Event Loop
1、宏任务:MacroTask
- script全部代码
- setTimeOut
- setInterval
- setImmediate
- I/O
- UI Rendering
2、微任务:MicroTask
- Process.nextTick(Node独有)
- Promise
- MutationObserver
浏览器中的Event Loop
JS有一个主线程main thread
和调用栈call-stack
,所有的任务哦度会被放到调用栈里等待主线程执行。
1、调用栈
采用后进先出
的原则, 当函数执行的时候,会被添加到栈的顶部,当操作完成后,就会从栈顶移除,直到栈内被清空。
2、 同步任务和异步任务
JS单线程任务分为:同步任务 and 异步任务
同步任务:在调用栈中按顺序等待主线程依次执行
异步任务:在异步任务有结果后,将注册的回调函数放入任务队列
中等待主线程空闲,被读取到栈内等待执行
任务队列
Task Queue
:队列,先进先出
3、事件循环的进程模型
- 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即null,则执行跳转到微任务的执行步骤
- 将事件循环中的任务设置为已选择任务
- 执行任务
- 将事件循环中当前运行任务设置为null
- 将已运行完成的任务从任务队列中删除
- microtasks步骤:进入microtask检查点
- 更新界面渲染
- 返回第一步
4、执行进入microtask检查点时,用户代理会执行以下步骤
- 设置microtask检查点标志为true
- 当事件循环microtask执行不为空时,选择一个最先进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask并运行,将已执行完的microtask设置为null,移除microtask
- 清理indexDB事务
- 设置进入microtask检查点标志为false
执行栈完成同步任务后,查看执行栈是否为空,如果执行栈为空,就会检查微任务队列是否为空,如果是空的话,执行宏任务,否则就一次性执行完所有的微任务。
举个例子🌰
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
第一次执行
执行同步代码,将宏任务和微任务划分到各自队列中
Tasks宏任务:run script、 setTimeout callback
Microtasks微任务:Promise then
JS stack: script
Log: script start、script end。
第二次执行
执行宏任务后,检测到微任务队列中不为空,执行Promise1,执行完后调用Promise2.then,放入到微任务中,再执行调用Promise.then
Tasks宏任务:run script、 setTimeout callback
Microtasks微任务:Promise2 then
JS stack: Promise2 callback
Log: script start、script end、promise1、promise2
第三次执行
当微任务队列为空时,执行宏任务,执行setTmeout callback,打印日志
Tasks宏任务:setTimeout callback
Microtasks微任务:
JS stack: setTimeout callback
Log: script start、script end、promise1、promise2、setTimeout
第四次执行
清空Tasks队列和JS Stack
Tasks宏任务:setTimeout callback
Microtasks微任务:
JS stack:
Log: script start、script end、promise1、promise2、setTimeout
浏览器中的Event Loop总结
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程遇到微任务,添加到微任务队列中
- 宏任务执行完毕后,立即执行微任务队列的所有微任务
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
Node中的Event Loop
Node中的Event Loop是基于libuv实现的,而libuv是Node的新跨平台抽象层,libuv使用异步、事件驱动的编程方式,核心是提供i/o事件循环和异步回调。
一共分为6个阶段:
1、timers:执行setTimeout和setInterval中到期的callback
执行这两个回调需要设置一个毫秒数,由于system的调度可能会延时,达不到预期时间。
2、pending callback:上一轮循环中少数的callback会放在这一阶段执行
此阶段执行某些操作系统回调。
3、idle,prepare:仅在内部使用
4、poll:最重要的阶段,执行pengding callback,在适当的情况下会阻塞在这个阶段
主要的两个功能:
执行I/O回调、处理轮询队列中的事件
当事件循环进入poll阶段并且在timers中没有可执行的定时器,将发生:
如果poll队列不为空,则事件循环将遍历其同步执行它们的callback队列,直到队列为空,或者达到system-dependent(系统相关限制)。
如果队列为空,将发生:
- 如果有
setImmediate()
回调需要执行,则会立即停止执行poll阶段并进入执行check阶段以执行回调 - 如果没有
setImmediate()
回调,poll阶段将等待callback被添加到队列中,然后立即执行
5、check:执行setImmediate
此阶段运训人员在poll阶段完成后立即执行回调。
如果poll阶段闲置并且script已排队setImmediate()
,则事件循环到达check阶段执行而不是继续等待。
setImmediate()
是一个特殊的计时器,它在事件循环的一个单独阶运行,使用libuv API
来调度在poll阶段完成后执行的回调。
通常,当代码被执行时,事件循环最终将达到poll阶段,它将等待传入连接,请求等。
但是如果已经调度了回调setImmediate(),并且轮询阶段变为空闲,则它将结束并且达到check阶段,而不是等到poll事件。
setImmediate() 的setTimeout()的区别
根据被调用的时间以不同的方式表现:
- setImmediate()设计用于在当前poll阶段完成后check阶段执行脚本
- setTimeout()安排在经过最小ms后运行的搅拌,在timers阶段执行
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
执行定时器的顺序将根据调用它们的上下文而有所不同。如果从主模块中调用两者,那么时间将收到进程性能的限制。其结果也不相同。
Process.nextTick()
Process.nextTick()
虽然是异步api的一部分,单不是事件循环的一部分。
Process.nextTick()
将callback
添加到next tick
队列。一旦当前事件轮询队列的任务全部完成,在next tick
队列中的所有callbacks
会被依次调用。
也就是说,如果存在nextTick队列,就会清空队列中的所有回调函数,并且优先于其他microtask执行。