什么是事件循环
- 每一个浏览器都至少有一个事件循环,一个事件循环至少有一个任务队列。
循环
指的是其永远处于一个“无限循环”中。不断将注册的回调函数推入到执行栈 - 浏览器的事件循环标准是由 HTML 标准规定的,而NodeJS中事件循环其实略有不同
为什么要事件循环
首先,我们看一段简单的代码:
function a1(){
console.log('1')
}
function a2(){
console.log('2')
}
function a3(){
console.log('3')
a1()
a2()
}
a3()
输出结果:
这段代码被执行的过程是什么样的呢?
首先,浏览器想要执行JS脚本,需要一个“东西”,将JS脚本(本质上是一个纯文本),变成一段机器可以理解并执行的计算机指令。这个“东西”就是JS引擎,它实际上会将JS脚本进行编译和执行.
v8引擎有两个非常核心的构成,执行栈
和堆
。执行栈中存放正在执行的代码,堆中存放变量的值,通常是不规则的。
V8运行此代码时,会首先调用 a3()
。 在 a3()
内部,会首先调用 a1()
,然后调用 a2()
当调用 a3 时,第一个帧被创建并压入栈中,帧中包含了
a1的参数和局部变量。 当
a3调用
a1时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含
a1的参数和局部变量。当
a1执行完毕然后返回时,第二个帧就被弹出栈(剩下
a3函数的调用帧 )当a3调用a2时,和第二帧同理,。当
a3` 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。
DOM 和 WEB API
js引擎可以帮助我们执行js脚本,但是我们的目标是“构建用户界面”而传统的前端界面是基于DOM构建的,DOM是文档对象模型,其提供了一些列可以供js直接调用的接口,除了DOM接口可以给js调用外,浏览器还提供了一些WEB API。DOM,WEB,API也罢,本质上都和JS没有关系.
V8是引擎,用来执行JS代码,浏览器和Node是JS的执行环境,其提供一些JS可以调用的API。
由于浏览器的存在,现在JS可以操作DOM和WEB API了,看起来是可以构建用户界面啦。 有一点需要提前讲清楚,V8只有栈和堆,其他诸如事件循环,DOM,WEB API它一概不知。原因前面其实已经讲过了,因为V8只负责JS代码的编译执行,你给V8一段JS代码,它就从头到尾一口气执行下去,中间不会停止。
多线程 无异步
在多请求无依赖的前提下,多线程同步,可以多个代码块同时执行,最理想的情况取决于最慢的情况。
单线程 异步(事件循环)
事件循环如何实现异步呢?
我们知道浏览器中JS线程只有一个,如果没有事件循环,就会造成一个问题。 即如果JS发起了一个异步IO请求,在等待结果返回的这个时间段,后面的代码都会被阻塞。 我们知道JS主线程和渲染进程是相互阻塞的,因此这就会造成浏览器假死。 如何解决这个问题? 一个有效的办法就是我们这节要讲的事件循环
。
其实事件循环就是用来做调度的,浏览器和NodeJS中的事件循坏就好像操作系统的调度器一样。
操作系统的调度器决定何时将什么资源分配给谁,
浏览器和NodeJS中的事件循环本质上也是做调度的,只不过调度的对象变成了JS的执行,事件循环决定了V8什么时候执行什么代码(V8只是负责JS代码的解析和执行其他它一概不知)。浏览器或者NodeJS中触发事件之后,到事件的监听函数被V8执行这个时间段的所有工作都是事件循环在起作用。
事件循环之所以可以实现异步,是因为碰到异步执行的代码“比如setTimeout,setInterval”,js调用浏览器的WEB API,同时浏览器开启计时,时间点到了,或者事件触发了,则将对应的回调函数放入队列中。
当主线程把调用栈中的程序“一口气”执行完后,要”换气“的时候,浏览器才会去检查队列里有没有要被处理的“消息”。如果有则将对应消息绑定的回调函数推入栈中。
微任务和宏任务
微任务micro-task
微任务(microtasks)其实是一个统称,包含了两部分:
- process.nextTick() (node专属) 注册的回调
- promise.then() 注册的回调
宏任务macro-task
- setTimeout 注册的回调
- setInterval 注册的回调
- setImmediate (node专属)注册的回调
- I/O 注册的回调
- script(整体代码)
- requestAnimationFrame (浏览器独有)
- UI rendering (浏览器独有)
浏览器的事件循环
浏览器事件循环流程图:
浏览器EventLoop的具体流程:
-
js引擎将所有代码放入执行栈,并依次弹出并执行,这些任务有的是同步有的是异步(宏任务或微任务)。
-
如果在执行 栈中代码时发现宏任务则交个浏览器相应的线程去处理,浏览器线程在正确的时机(比如定时器最短延迟时间)将宏任务的消息(或称之为回调函数)推入宏任务队列。而宏任务队列中的任务只有执行栈为空时才会执行。
-
如果执行 栈中的代码时发现微任务则推入微任务队列,和宏任务队列一样,微任务队列的任务也在执行栈为空时才会执行,但是微任务始终比宏任务先执行。
-
当执行栈为空时,eventLoop转到微任务队列处,依次弹出首个任务放入执行栈并执行,如果在执行的过程中又有微任务产生则推入队列末尾,这样循环直到微任务队列为空。
-
当执行栈和微任务队列都为空时,eventLoop转到宏任务队列,并取出队首的任务放入执行栈执行。需要注意的是宏任务每次循环只执行一个。
-
重复1-5过程
-
…直到栈和队列都为空时,代码执行结束。引擎休眠等待直至下次任务出现。
注意:
- 宏任务每次只取一个,执行之后马上执行微任务。
- 微任务会依次执行,直到微任务队列为空。
node事件循环
node事件流程的简单示意图
node中的宏任务
- Timers Queue
- IO Callbacks Queue
- Check Queue
- Close Callbacks Queue
node中的微任务
- Next Tick Queue:是放置process.nextTick(callback)的回调任务
- Other Micro Queue:放置其他microtask,比如Promise等
nodejs的宏任务队列
- timers阶段:初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 I/O callbacks 阶段。
- I/O callback阶段:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks。
- idle, prepare阶段:仅node内部使用。
- poll阶段:获取新的I/O事件,适当的条件下node将阻塞在这里。
- check阶段:执行setImmediate()设定的callbacks。(会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。)
- close callbacks阶段:执行一些关闭回调,比如:socket.on(‘close’, …)等
Node的 EventLoop的具体流程
- 执行全局Script的同步代码
- 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务。
- 执行macrotask宏任务,共6个阶段,从第一阶段开始,依次执行到第6个阶段,Node 11及其之后,针对事件循环的每一个阶段,微任务的执行顺序进行了统一,在每次调用回调之后,就执行相应微任务,不会等到所有回调执行完毕后才执行。
- 重复1 - 3过程。
事件循环的总结
- 事件循环是 浏览器 和 Node 执行JS代码的核心机制,但浏览器 和 NodeJS事件循环的实现机制有些不同。
- 浏览器事件循环有一个宏队列,一个微队列,且微队列在执行过程中一个接一个执行一直到队列为空,宏队列只取队首的一个任务放入执行栈执行,执行过后接着执行微队列,并构成循环。
- Node 11及其之后,针对事件循环的每一个阶段,微任务的执行顺序进行了统一,在每次调用回调之后,就执行相应微任务,不会等到所有回调执行完毕后才执行。