事件循环的主要概念
事件循环是 js
的调度机制,所有的脚本,事件,渲染代码(任务)都需要通过事件循环来安排执行。
循环中的任务分为宏任务
,微任务
两种队列,主要的循环逻辑是:
- 先从
宏任务
队列中取出一个任务处理 - 然后执行整个
微任务
队列,如果其间有产生了微任务
,继续执行,直到微任务
队列为空。 - 回到阶段
1
,如此循环往复,定期轮询。
宏任务
, 微任务
队列
- 微任务队列:
nextTickQueue
队列:process.nextTick
microTaskQueue
队列: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
阶段: 执行setImmediate
close 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》