浏览器和node事件循环

什么是事件循环

  • 每一个浏览器都至少有一个事件循环,一个事件循环至少有一个任务队列。循环指的是其永远处于一个“无限循环”中。不断将注册的回调函数推入到执行栈
  • 浏览器的事件循环标准是由 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的具体流程:

  1. js引擎将所有代码放入执行栈,并依次弹出并执行,这些任务有的是同步有的是异步(宏任务或微任务)。

  2. 如果在执行 栈中代码时发现宏任务则交个浏览器相应的线程去处理,浏览器线程在正确的时机(比如定时器最短延迟时间)将宏任务的消息(或称之为回调函数)推入宏任务队列。而宏任务队列中的任务只有执行栈为空时才会执行。

  3. 如果执行 栈中的代码时发现微任务则推入微任务队列,和宏任务队列一样,微任务队列的任务也在执行栈为空时才会执行,但是微任务始终比宏任务先执行。

  4. 当执行栈为空时,eventLoop转到微任务队列处,依次弹出首个任务放入执行栈并执行,如果在执行的过程中又有微任务产生则推入队列末尾,这样循环直到微任务队列为空。

  5. 当执行栈和微任务队列都为空时,eventLoop转到宏任务队列,并取出队首的任务放入执行栈执行。需要注意的是宏任务每次循环只执行一个。

  6. 重复1-5过程

  7. …直到栈和队列都为空时,代码执行结束。引擎休眠等待直至下次任务出现。

    注意:

  • 宏任务每次只取一个,执行之后马上执行微任务。
  • 微任务会依次执行,直到微任务队列为空。

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及其之后,针对事件循环的每一个阶段,微任务的执行顺序进行了统一,在每次调用回调之后,就执行相应微任务,不会等到所有回调执行完毕后才执行。

猜你喜欢

转载自blog.csdn.net/weixin_43183219/article/details/124187552