事件循环的主要概念
事件循环是 js 的调度机制,所有的脚本,事件,渲染代码(任务)都需要通过事件循环来安排执行。
循环中的任务分为宏任务,微任务两种队列,主要的循环逻辑是:
- 先从
宏任务队列中取出一个任务处理 - 然后执行整个
微任务队列,如果其间有产生了微任务,继续执行,直到微任务队列为空。 - 回到阶段
1,如此循环往复,定期轮询。
宏任务, 微任务 队列
- 微任务队列:
nextTickQueue队列:process.nextTickmicroTaskQueue队列:Promise,MutationObserver- 注意⚠️:执行时
nextTickQueue队列先清空后,再清空microTaskQueue队列。 - 注意⚠️:
MutationObserver事件类似setInterval,添加到队列的同类任务没被处理之前不会再次插入。
- 宏任务队列:
- 除上述微任务之外的都是宏任务:"事件、用户交互、脚本、渲染、网络等"。
- 注意⚠️:页面渲染也是
宏任务,特殊的是渲染引擎与JS引擎是互斥的,调度的时候还多了一个切换引擎的成本。
node中分阶段的宏任务
浏览器中的宏任务没有阶段的区分,只会按照回调函数进入事件队列的顺序进行执行,node则会按照不同类型的回调函数在不同阶段有区别地执行。
node 的分阶段轮询机制
-
每个操作阶段:
- 都会先执行这个阶段的特定操作。
- 然后执行其
FIFO的队列中任务(每个阶段都有一个FIFO的队列, 直到队列为空或者达到最大数量的执行限制【每次循环估计有最大的时间限制】)。 - 最后移动到下一阶段。
-
主要轮询阶段按顺序如下:
timers阶段:执行到期的setTimeout、setInterval。pending callbacks阶段:执行已经积压的I/O事件。poll阶段(⚠️此阶段有一段时间的同步阻塞 ):- 计算阻塞时间
- 从系统内核检索新的
I/O事件添加到队列,并执行(执行除了close callback,timers,setImmediate事件之外的几乎所有事件) - 猜测:会将
close callback,timers,setImmediate事件分配到对应的队列(所以timers阶段setTimeout嵌套不会连续执行,因为子setTimeout还没插入队列)。 - 如果队列为空:
- 如果有
setImmediate,进入下一check阶段 - 如果没有
setImmediate,close callback事件,且有timmer到期,将其添加到timmers,执行timmers队列的事件(并不是回退到timmers阶段)。
- 如果有
check阶段: 执行setImmediateclose callback:执行close事件,如socket.on('close', ...)。
-
源代码如下:
js/** * 可以看出,setTimeout绑定的回调函数有可能会在两个阶段被推到主线程中执行。 * 但是因一个在前,一个在尾部,其实也可以认为又开启了一次新的轮询。 */ while (r != 0 && loop->stop_flag == 0) { // first timers uv__update_time(loop); uv__run_timers(loop); ran_pending = uv__run_pending(loop); uv__run_idle(loop); uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) { timeout = uv_backend_timeout(loop); } uv__io_poll(loop, timeout); uv__run_check(loop); uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { // second timers uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; }
实际应用
-
setImmediate()为什么比setTimeout(callback, 0)快?- 因为虽然
setTimeout/setInterval的时间设置为0,但是实际上系统会将其设置为1。 poll阶段处理函数中的setImmediate肯定比setTimeout/setInterval早执行。因为下一阶段就是check阶段。
js/** * 输出顺序: * setImmediate * setTimeout * * *这说明timmers执行期间添加的timer并不会像微任务队列那样接着执行,* * 也可能到期后需要其他阶段来辅助添加到队列,如poll阶段才会分配到队列, * 因此导致setImmediate总是比setTimeout先执行。 */ setTimeout(() => { setTimeout(() => { console.log('setTimeout'); },0); setImmediate(() => { console.log('setImmediate'); }); // 中断10ms const now = new Date().getTime(); while (true) { if (new Date().getTime() - now > 100) { break; } } }, 100); - 因为虽然
-
setTimeout/setInterval- 时间设置为0,但是系统会将其设置为1ms。如果嵌套超过4层,则系统会将其设置为4ms。
- 可以使用
setImmediate代替setTimeout。
-
postMessage延迟比setTimeout(callback, 0)更快的原因应该是poll阶段只是分配了setTimeout(callback, 0)但没执行,而postMessage的事件被执行了。
参考
"为了协调事件、用户交互、脚本、渲染、网络等,用户代理必须使用本节中描述的事件循环"--《HTML Standard》