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

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

时间:2023-04-03 21:23:32 Node.js

浏览器和Node的事件循环有什么区别?一、线程与进程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渲染。当任务队列空闲时,主线程会执行GUI渲染。2.JS引擎线程这个线程当然主要负责处理JavaScript脚本,执行代码。它还主要负责执行准备执行的事件,即当定时器计数结束,或者异步请求成功并正确返回时,依次进入任务队列,等待JS的执行引擎线程。当然,这个线程和GUI渲染线程是互斥的。当JS引擎线程执行JavaScript脚本的时间过长,会导致页面渲染被阻塞。3.定时器触发线程负责执行异步定时器等函数的线程,如:setTimeout,setInterval。主线程在顺序执行代码的时候,遇到定时器,就会把定时器交给线程处理。计数完成后,事件触发线程会将计数的事件添加到任务队列的尾部,等待JS引擎线程执行。4、事件触发线程主要负责将准备好的事件传递给JS引擎线程执行。例如当setTimeout定时器计时结束,ajax等异步请求成功并触发回调函数,或者当用户触发点击事件时,线程会依次将ready-to-go事件添加到任务尾部队列,等待JS引擎线程执行。5.异步http请求线程负责执行异步请求等函数的线程,如:Promise、axios、ajax等,当主线程依次执行代码时,遇到异步请求,会递交把函数交给线程进行处理。当检测到状态码变化时,如果有回调函数,事件触发线程会将回调函数添加到任务队列的尾部,等待JS引擎线程执行。三、浏览器中的EventLoop1、Micro-Task和Macro-Task浏览器端的EventLoop中有两类异步队列:macro(宏任务)队列和micro(微任务)队列。可以有多个宏任务队列,只有一个微任务队列。常见的宏任务如:setTimeout、setInterval、script(整体代码)、I/O操作、UI渲染等。常见的微任务如:newPromise().then(回调)、MutationObserver(html5新特性)),etc.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.Node中的EventLoop1.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阶段:该阶段执行定时器的回调I/O回调(setTimeout,setInterval)Phase:处理一些上个周期未执行的I/O回调idle,preparephase:只有节点内部使用pollPhase:获取新的I/O事件,under合适的条件,节点会阻塞在这里检查阶段:执行setImmediate()回调关闭回调阶段:执行套接字关闭事件回调注意:以上六个阶段不包括process.nextTick()(下面会介绍)接下来介绍timers、poll、check三个阶段详解,因为日常开发中大部分的异步任务都是在这三个阶段处理的。(1)timertimers阶段执行setTimeout和setInterval回调,受poll阶段控制。同样,Node中定时器指定的时间也不是准确时间,只能尽快执行。(2)pollpoll是一个很关键的阶段,在这个阶段,系统会做两件事1.回到timer阶段执行回调2.执行I/O回调,如果进入这个阶段时没有设置timer如果poll队列不为空,会发生下面两件事如果poll队列不为空,会遍历回调队列,同步执行,直到队列为空或者达到系统限制如果poll队列为空,则两个如果有setImmediate回调需要执行,poll阶段会停止,进入check阶段执行回调。如果没有要执行的setImmediate回调,它会等待回调被添加到队列中并立即执行回调。还会有超时设置,防止永远等待。当然如果设置了timer并且poll队列为空,它会判断是否有timer超时,如果有则返回timer阶段执行回调。(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和browserEventLoop的区别后面会详细说)以下)。3、Micro-Task和Macro-TaskNode的事件循环中也有两类异步队列:macro(宏任务)队列和micro(微任务)队列。常见的宏任务如:setTimeout、setInterval、setImmediate、脚本(整体代码)、I/O操作等。常见的微任务如:process.nextTick、newPromise().then(callback)等。4.注意点(1)setTimeout和setImmediate很相似,主要区别在于调用的时机。setImmediate设计在poll阶段完成时执行,即check阶段;setTimeout设计是在poll阶段空闲,设定时间到达时执行,但在timer阶段执行(){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浏览器端处理过程如下:节点端运行结果分为两种情况:如果是node11版本,一次宏任务(setTimeout、setInterval和setImmediate)在一个阶段执行完后,微任务会立即执行,任务队列与浏览器的操作一致。最后的结果是timer1=>promise1=>timer2=>promise2。在完成队列中。如果第二个定时器还没有在完成队列中,那么最终结果是timer1=>promise1=>timer2=>promise2如果第二个定时器已经在完成队列中,那么最终结果是timer1=>timer2=>promise1=>promise2(下面的流程解释都是基于这种情况)1.全局脚本(main())执行,依次将两个定时器放入定时器队列,main()执行完后,调用栈空闲,任务队列开始执行;2、首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样步骤执行timer2,打印timer2;3、至此timer阶段执行结束,事件循环进入下一阶段之前,执行microtask队列中的所有任务,依次打印promise1和promise2。Node端的处理流程如下:6.总结在浏览器和Node环境下,microtask任务队列的执行时机在Node端是不同的,microtask是在事件循环的各个阶段之间执行的浏览器端在执行过程中,微任务在事件循环的宏任务执行完之后执行。