什么是Event Loop?
js是单线程语言,代码需要一句一句往下执行,会出现阻塞。Event Loop是解决这一问题的机制,让js实现异步,在不同的js执行环境中(浏览器、node)表现不同。
浏览器中的Event Loop
js自上而下执行代码
同步任务,在调用栈中按照顺序等待主线程依次执行
异步任务,在EventTable中注册回调事件,等异步任务有结果后,将注册的回调函数放入对应的任务队列中
任务队列有宏任务队列和微任务队列。
一个线程仅有一个微任务队列,微任务包括:promise.then/catch/final、Mutation Observer、process.nextTick(node)。除此之外的事件,均为宏任务。
一个线程可以有多个宏任务队列分别处理不同类型的事件,如鼠标键盘事件可放在一个宏任务队列中,优先执行保证用户体验,其他类型事件可分别放在不同的宏任务队列中。
执行顺序:一个宏任务 -> 全部微任务 -> 一个宏任务 -> 全部微任务 -> ...直至两个队列都为空。同步任务相当于一个宏任务,在最开始执行。
promise.xxx属于es6范畴,规范中对于其是否视为微任务并不明晰,所以各浏览器实现上有所不同,影响执行顺序。Google将其视为微任务,Safari和Firefox将其视为宏任务。
async await是promise的语法糖,可转为promise.then来看待。
node中的Event Loop
node的Event Loop一共分为6个阶段,每个细节具体如下:
- timers:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
- pending callbacks:执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- poll:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
- check:setImmediate() 回调函数在这里执行。
- close callbacks:一些关闭的回调函数,如:socket.on('close', ...)。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
process.nextTick事件单独有一个队列存放,当每一阶段执行完毕后,先执行process.nextTick queue里的全部,再执行micro queue里的全部,然后再进入下一阶段。
为了和浏览器更加趋同,node v11版本将timer阶段的setTimeout,setInterval...和在check阶段的immediate改为一旦执行一个阶段里的一个任务就立刻执行微任务队列。node v10版本还是保持自己的执行方式。
对比总结
浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。