当前位置: 首页 > 后端技术 > Node.js

浏览器与Node的事件循环(Event Loop)有何区别-

时间:2023-04-03 10:16:12 Node.js

浏览器和Node的事件循环有什么区别?前言在这篇文章中,我们将介绍JS实现异步的原理,并了解到浏览器中的EventLoop和Node中的EventLoop其实是不一样的。一、线程与进程1、概念我们常说JS是单线程执行的,也就是说一个进程中只有一个主线程,那么什么是线程呢?什么是流程?官方的说法是:进程是CPU资源分配的最小单位;线程是CPU调度的最小单位。这两句话不太好理解。先看图:进程就像图中的一个工厂,有自己的工厂资源。线程就像图中的工人。多个工人在一个工厂里一起工作,工厂和工人之间是1:n的关系。也就是说,一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路径;factory空间是worker共享的,也就是说一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。多个工厂独立存在。2、多进程和多线程multi-process:如果允许两个或多个进程同时运行在同一个计算机系统中。多进程的好处是显而易见的。比如你可以边听歌边打开编辑器敲代码,编辑器和听歌软件的进程完全不会相互干扰。多线程:程序中包含多个执行流,即一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行线程来完成各自的任务。以Chrome浏览器为例,当你打开一个Tab页面时,实际上是创建了一个进程,一个进程中可以有多个线程(下面详细介绍),比如渲染线程、JS引擎线程、HTTP请求线程等。等待。当你发起一个请求时,你实际上创建了一个线程。当请求结束时,线程可能会被销毁。2、浏览器内核浏览器内核简单的说就是获取页面内容,将信息组织起来(应用CSS),计算组合,最后输出一个可视化的图像结果,通常称为渲染引擎。浏览器内核是多线程的。在内核的控制下,各个线程相互协作,保持同步。一个浏览器通常由以下常驻线程组成:GUI渲染线程JavaScript引擎线程定时触发线程事件触发线程异步http请求线程1.GUI渲染线程主要负责页面渲染、解析HTML、CSS、构建DOM树、布局和绘图等。当界面需要重绘或者某些操作触发回流时,会执行该线程。该线程与JS引擎线程互斥。JS引擎线程执行时,会暂停GUI渲染。当任务队列空闲时,JS引擎将执行GUI渲染。2.JS引擎线程这个线程当然主要负责处理JavaScript脚本,执行代码。它还主要负责执行准备执行的事件,即当定时器计数结束,或者异步请求成功并正确返回时,依次进入任务队列,等待JS的执行引擎线程。当然,这个线程和GUI渲染线程是互斥的。当JS引擎线程执行JavaScript脚本的时间过长,会导致页面渲染被阻塞。3.定时器触发线程负责执行异步定时器等函数的线程,如:setTimeout,setInterval。主线程在顺序执行代码的时候,遇到定时器,就会把定时器交给线程处理。计数完成后,事件触发线程会将计数的事件添加到任务队列的尾部,等待JS引擎线程执行。4.事件触发线程主要负责将准备好的事件交给JS引擎线程执行。比如当setTimeout定时器倒计时,ajax等异步请求成功并触发回调函数,或者当用户触发点击事件时,线程会依次将准备好的事件添加到任务队列的尾部,等待JS引擎线程的执行。5.异步http请求线程负责执行异步请求等函数的线程,如:Promise、axios、ajax等,当主线程依次执行代码时,遇到异步请求,会递交把函数交给线程进行处理。当检测到状态码变化时,如果有回调函数,事件触发线程会将回调函数添加到任务队列的尾部,等待JS引擎线程执行。三、浏览器中的EventLoop1.Micro-Task和Macro-Task事件循环中有两种异步队列:macro(宏任务)队列和micro(微任务)队列。可以有多个宏任务队列,只有一个微任务队列。常见的宏任务如:setTimeout、setInterval、setImmediate、script(整体代码)、I/O操作、UI渲染等。常见的微任务如:process.nextTick、newPromise().then(回调)、MutationObserver(html5新特性)等2.EventLoop流程分析一个完整的EventLoop流程,可以概括为以下几个阶段:一开始,执行栈是空的,我们可以把执行栈看成是一个存放函数调用的栈结构,如下先进后出的原则。微队列为空,宏队列中只有一个script脚本(整体代码)。全局上下文(脚本标签)被压入执行栈,同步代码执行。在执行过程中会判断是同步任务还是异步任务。通过调用一些接口,可以产生新的宏任务和微任务,并将它们推入各自的任务队列。同步代码执行完毕后,脚本脚本将从宏队列中移除。这个过程本质上就是队列宏任务执行和出队的过程。上一步我们dequeued的是一个宏任务,这一步我们处理的是一个微任务。但需要注意的是,当宏任务出队时,任务是一个一个执行的;当微任务出队时,任务被一个接一个地执行。因此,我们处理微队列这一步,将队列中的任务一个一个执行,并出队,直到队列清空。执行渲染操作,更新接口检查是否有Webworker任务,有则处理。重复上述过程,直到两个队列都被清除。总结一下,每个循环都是这样一个过程:当一个宏任务执行完后,会检查是否有微任务队列。如果有,则先执行微任务队列中的所有任务,如果没有,则读取宏任务队列中的最前面的任务。macrotask在执行过程中,如果遇到microtask,会依次加入到microtask队列中。栈为空后,再次读取微任务队列中的任务,以此类推。下面我们看一个例子来介绍一下上面的过程:Promise.resolve().then(()=>{console.log('Promise1')setTimeout(()=>{console.log('setTimeout2')},0)})setTimeout(()=>{console.log('setTimeout1')Promise.resolve().then(()=>{console.log('Promise2')})},0)最后输出就是Promise1,setTimeout1,Promise2,setTimeout2开始执行栈的同步任务(这个是宏任务),执行完之后会检查是否有微任务队列,微任务队列存在于上题中(只有一个)),然后执行所有的微任务队列任务输出Promise1,同时会产生一个宏任务setTimeout2然后进入宏任务队列。宏任务setTimeout1会在setTimeout2之前执行宏任务setTimeout1,输出setTimeout1会在执行宏任务setTimeout1时生成微任务Promise2,放入微任务队列中,然后先清除微任务队列中的所有任务,然后输出Promise2。清空微任务队列中的所有任务后,会去宏任务队列中再取一个。这次执行setTimeout2。4.节点中的事件循环1。Node简介Node中的EventLoop与浏览器中的EventLoop完全不同。Node.js使用V8作为js的解析引擎,使用自己设计的libuv进行I/O处理。libuv是一个事件驱动的跨平台抽象层,封装了不同操作系统的一些底层特性,对外提供统一的API,事件循环机制也在其中实现(下面详细介绍)。Node.js的运行机制是这样的:V8引擎解析JavaScript脚本。解析后的代码调用NodeAPI。libuv库负责执行NodeAPI。它将不同的任务分配给不同的线程,形成一个EventLoop(事件循环),并将任务的执行结果以异步的方式返回给V8引擎。然后V8引擎将结果返回给用户。2.六个阶段libuv引擎中的事件循环分为6个阶段,它们会按顺序重复运行。每当进入某个阶段,就会从对应的回调队列中取出函数执行。当队列为空或回调函数执行次数达到系统设定的阈值时,进入下一阶段。从上图我们可以大致看出node中事件循环的顺序:外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(closecallback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/Ocallbacks)-->空闲阶段(idle,prepare)-->轮询阶段(按此顺序反复运行)...timers阶段:该阶段执行timer的回调(setTimeout,setInterval)I/O回调阶段:处理上一个周期idle中一些未执行的I/O回调,prepare阶段:仅供节点内部使用poll阶段:获取新的I/O事件,下合适的条件,节点会阻塞在这里检查阶段:执行setImmediate()回调关闭回调阶段:执行套接字关闭事件回调注意:以上六个阶段不包括process.nextTick()(下面会介绍)接下来介绍timers、poll、check三个阶段详解,因为日常开发中大部分的异步任务都是在这三个阶段处理的。(1)timertimers阶段执行setTimeout和setInterval回调,受poll阶段控制。同样,Node中定时器指定的时间也不是准确时间,只能尽快执行。(2)pollpoll是一个关键阶段。在这个阶段,系统会做两件事。回到定时器阶段执行回调。执行I/O回调,如果进入这个阶段时没有设置定时器,会发生poll队列不为空,下面两件事会遍历回调队列,同步执行,直到队列为空或者到达系统限制。如果轮询队列为空,则会发生两件事。如果有setImmediate回调需要执行,poll阶段会Stop,进入check阶段执行回调。如果没有setImmediate回调执行,它会等待回调被添加到队列中并立即执行回调。还会有一个超时设置,防止它等待。当然,如果设置了定时器,并且poll队列为空,则会判断是否有定时器超时,如果有则返回定时器阶段执行回调。(3)check阶段setImmediate()的回调会加入到check队列中。从事件循环的阶段图可以知道,check阶段的执行顺序是在poll阶段之后。我们先来看一个例子:console.log('start')setTimeout(()=>{console.log('timer1')Promise.resolve().then(function(){console.log('promise1')})},0)setTimeout(()=>{console.log('timer2')Promise.resolve().then(function(){console.log('promise2')})},0)Promise.resolve().then(function(){console.log('promise3')})console.log('end')//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2beginning执行栈的同步任务(这个是宏任务)执行完后(依次打印出startend,依次将2个定时器放入定时器队列),先执行微任务(这个是一样的作为浏览器端),所以打印出promise3,然后进入timers阶段,执行timer1的回调函数,打印timer1,并将promise.then回调放入microtask队列,同样步骤执行timer2,打印timer2;这个和浏览器端有很大的不同,timers阶段有几个setTimeout/setInterval会依次执行,不像浏览器端是每执行完一个macrotask就执行一个microtask(Node和浏览器EventLoop的区别后面会详细介绍以下)。3.注意点(1)setTimeout和setImmediate很相似,主要区别在于调用时机。setImmediate设计在poll阶段完成时执行,即check阶段;setTimeout设计是在poll阶段空闲,设置时间到了的时候执行,但是在timer阶段执行setTimeout(functiontimeout(){console.log('timeout');},0);setImmediate(function立即(){console.log('立即');});对于上面的代码,setTimeout可以先执行也可以后执行。首先是setTimeout(fn,0)===setTimeout(fn,1),这是源码决定的。进入事件循环也需要成本。如果准备时间超过1ms,则直接在timer阶段执行。如果setTimeout回调准备时间小于1ms,那么先执行setImmediate回调,但是在异步i/o回调内部调用这两者时,总是先执行setImmediate,然后setTimeoutconstfs=require('fs')fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout');},0)setImmediate(()=>{console.log('immediate')})})//immediate//timeout在上面的代码中,setImmediate总是先执行。因为两段代码都写在了IO回调中,IO回调是在poll阶段执行的。执行回调时,队列为空,发现有setImmediate回调,于是直接跳转到check阶段执行回调。(2)函数process.nextTick实际上是独立于EventLoop的。它有自己的队列。当每个阶段完成后,如果有nextTick队列,队列中的所有回调函数将被清除,并优先于其他微任务执行。setTimeout(()=>{console.log('timer1')Promise.resolve().then(function(){console.log('promise1')})},0)process.nextTick(()=>{console.log('nextTick')process.nextTick(()=>{console.log('nextTick')process.nextTick(()=>{console.log('nextTick')process.nextTick(()=>{console.log('nextTick')})})})})//nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise15.Node和浏览器EventLoop的区别在浏览器环境下,microtask的任务队列在每个macrotask执行完之后执行。在Node.js中,microtask会在eventloop的各个stage之间执行,也就是一个stage执行完之后,microtask队列中的task就会被执行。下面我们用一个例子来说明两者的区别:setTimeout(()=>{console.log('timer1')Promise.resolve().then(function(){console.log('promise1')})},0)setTimeout(()=>{console.log('timer2')Promise.resolve().then(function(){console.log('promise2')})},0)浏览器端运行结果:timer1=>promise1=>timer2=>promise2浏览器端处理流程如下:节点端运行结果:timer1=>timer2=>promise1=>promise2执行全局脚本(main()),并把2个定时器依次进入定时器队列,main()执行完毕,调用栈空闲,任务队列开始执行;首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样步骤执行timer2,打印timer2;至此,timer阶段的执行结束,在事件循环进入下一阶段之前,执行完microtask队列中的所有任务,依次打印promise1和promise2。Node端的处理流程如下:6.总结在浏览器和Node环境下,microtask任务队列的执行时机是不同的Node在浏览器端,microtask是在事件循环的各个阶段之间执行的。在浏览器端,microtask在事件循环中的macrotask执行完之后执行。指浏览器进程?线?傻傻分不清!TheEventLoopMechanism前端性能优化原理与前端面试实践之EventLoop机制深入理解JS事件循环机制(Node.js)JavaScript事件循环(EventLoop)机制详解-loop-timers-and-nexttick关于FundebugFundebug专注于JavaScript,微信小程序,微信小游戏,支付宝小程序,ReactNative,Node.js和Java在线应用实时BUG监控自2016年双十一正式上线以来,Fundebug累计处理了9亿+错误事件。付费客户包括谷歌、360、金山、人民网等众多品牌公司。欢迎大家免费试用!转载版权声明请注明作者Fundebug及本文地址:https://blog.fundebug.com/2019/01/15/diffrences-of-browser-and-node-in-event-loop/