为了协调事件、用户交互、脚本、UI 渲染、网络请求,用户代理必须使用 事件循环机制(Event Loop)。
这种事件循环机制是由 JavaScript 的宿主环境来实现的,在浏览器运行环境中由浏览器内核引擎实现,而在 NodeJS 中则由 libuv 引擎实现。
主线程运行时候,产生堆(Heap)和栈(Stack),栈中的代码调用各种外部 API,它们在任务队列中加入各种事件。只要栈中的代码执行完毕,主线程就会通过事件循环机制读取任务队列,依次执行那些事件所对应的回调函数。
运行机制:
JavaScript 的异步任务根据事件分类分为两种:宏任务(MacroTask)和微任务(MicroTask)
宏任务与微任务的区别在于队列中事件的执行优先级。进入整体代码(宏任务)后,开始首次事件循环,当执行上下文栈清空后,事件循环机制会优先检测微任务队列中的事件并推至主线程执行,当微任务队列清空后,才会去检测宏任务队列中的事件,再将事件推至主线程中执行,而当执行上下文栈再次清空后,事件循环机制又会检测微任务队列,如此反复循环。
宏任务与微任务的优先级
<script>
标签的代码是第一个宏任务process.nextTick
优先级高于 Promise.then
🌰 代码示例:
console.log(1);setTimeout(() => {console.log(2);}, 0);let promise = new Promise((res) => {console.log(3);resolve();}).then((res) => {console.log(4);}).then((res) => {console.log(5);});console.log(6);// 1 3 6 4 5 2
在 Node 中,事件循环表现出的状态与浏览器中大致相同。不同的是 Node 中有一套自己的模型。Node 中事件循环的实现是依靠的 libuv 引擎。我们知道 Node 选择 Chrome V8 引擎作为 JavaScript 解释器,V8 引擎将 JavaScript 代码分析后去调用对应的 Node API,而这些 API 最后则由 libuv 引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上 Node 中的事件循环存在于 libuv 引擎中。
┌───────────────────────┐┌─>│ timers ││ └──────────┬────────────┘│ ┌──────────┴────────────┐│ │ I/O callbacks ││ └──────────┬────────────┘│ ┌──────────┴────────────┐│ │ idle, prepare ││ └──────────┬────────────┘ ┌───────────────┐│ ┌──────────┴────────────┐ │ incoming: ││ │ poll │<──connections─── ││ └──────────┬────────────┘ │ data, etc. ││ ┌──────────┴────────────┐ └───────────────┘│ │ check ││ └──────────┬────────────┘│ ┌──────────┴────────────┐└──┤ close callbacks │└───────────────────────┘
setImmediate
的回调会在这个阶段执行close
事件、定时器和 setImmediate()
的回调当一个消息需要太长时间才能处理完毕时,Web 应用就无法处理用户的交互,例如点击或滚动。浏览器用程序需要过长时间运行的对话框来缓解这个问题。一个很好的做法是缩短消息处理,并在可能的情况下将一个消息裁剪成多个消息。