简介
事件循环在浏览器和node环境机制不同, 一个是h5规范的实现, 一个集成libuv库的功能. 理解事件循环对js异步非阻塞的理解有会有所帮助.
在浏览器里每个web worker都有自己的事件循环系统.
概念
- js只有一个线程:避免DOM渲染冲突.
- 浏览器需要渲染DOM
- JS可以修改DOM结构
- JS执行的时候,浏览器DOM渲染会暂停
- 两端JS也不能同时执行(都修改DOM就冲突了)
- webworker支持多线程,但是不能访问DOM
- js运行时包含call stack(回调要执行了就放入栈里), heap, task queue.

- 事件队列,分为
- micro taskqueue: Promise的then, MutationObserver, process.nextTick
- macro taskqueue: setInterval, setTimeout, setImmediate(ie), MessageChannel, dispatching event(js执行node.click()方法不算)
浏览器环境
执行过程:
- 执行完主执行线程中的任务。
- 取出Microtask Queue中任务执行直到清空。
- 取出Macrotask Queue中一个任务执行。
- 取出Microtask Queue中任务执行直到清空。
- 重复3和4。
note
- HTML中每一个正在被解析script标签中的代码是一个独立的取出Macrotask. 即会执行完前面的script中创建的microtask再执行后面的script中的同步代码
- promise的回调then和catch才是microtask,本身的内部代码不是。
- microtask执行过程中产生的microtask会添加队尾然后一并执行.
- 同步代码执行时遇到异步代码, 会加入到任务队列中. 同步执行完后, 刷一遍microtask queue. 执行一个macro task, 再刷一遍microtask queue. 如此反复.
- pseudo code1234while (true) {宏任务队列.shift()()微任务队列全部任务()}
demo
|
|
output12345678910111213141516script1 执行完主执行线程中的任务。script1-mi0 取出Microtask Queue中任务执行直到清空。script2 // 等等前面script的microtaskscript2-mi0 script1-ma1 取出Macrotask Queue中一个任务执行。script1-mi1script1-mi2 取出Microtask Queue中任务执行直到清空。script1-ma2 取出Macrotask Queue中一个任务执行。script1-mi3script1-mi4 取出Microtask Queue中任务执行直到清空。script2-ma1 script2的macro task后添加后执行script2-mi1script2-mi2script2-ma2script2-mi3script2-mi4
node环境
事件循环6个阶段执行顺序:
- When the queue has been exhausted or the callback limit is reached, the event loop will move to the next phase, and so on.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859┌───────────────────────────┐┌─>│ timers ││ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐│ │ pending callbacks ││ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐│ │ idle, prepare ││ └─────────────┬─────────────┘ ┌───────────────┐│ ┌─────────────┴─────────────┐ │ incoming: ││ │ poll │<─────┤ connections, ││ └─────────────┬─────────────┘ │ data, etc. ││ ┌─────────────┴─────────────┐ └───────────────┘│ │ check ││ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐└──┤ close callbacks │└───────────────────────────┘```1. timers:执行setTimeout() 和 setInterval()中到期的callback。1. I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行1. idle, prepare:仅内部使用1. poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段1. check:执行setImmediate的callback1. close callbacks:执行close事件的callback,例如socket.on("close",func)### 循环之前在进入第一次循环之前,会先进行如下操作:- 同步任务- 发出异步请求- 规划定时器生效的时间- 执行process.nextTick()### 开始循环按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空nextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:- 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。- 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。- 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。- 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。进入下轮循环。可以看出,nextTick优先级比promise等microtask高。setTimeout和setInterval优先级比setImmediate高。### 注意- 如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。- setTimeout优先级比setImmediate高,但是由于setTimeout(fn,0)的真正延迟不可能完全为0秒,可能出现先创建的setTimeout(fn,0)而比setImmediate的回调后执行的情况。- 伪代码```jswhile (true) {loop.forEach((阶段) => {阶段全部任务()nextTick全部任务()microTask全部任务()})loop = loop.next}
demo
|
|
node执行后结果123456789101112setTimeout - 1 1s oversetTimeout - 2 //1、2为单阶段task1s oversetTimeout - 1 - thensetTimeout - 2 - thensetTimeout - 1 - then - thensetTimeout - 2 - then - thensetTimeout - 1 - 11s oversetTimeout - 2 - 11s over