2023年7月7日发(作者:)
详解浏览器和Node的事件循环机制及区别关于事件循环机制(详解)前⾔JS是单线程的脚本语⾔,即在同⼀时间只能做⼀件事。为了协调时间、⽤户交互、脚本、UI渲染和⽹络处理等⾏为,防⽌主线程堵塞,这才有了事件循环机制。前⾯提到了JS是单线程的脚本语⾔,那么问题来了,为什么JS是单线程的呢?JS作为主要运⾏在浏览器的脚本语⾔,主要的⼯作之⼀就是操作DOM。若JS有两个线程的话,那么如果这两个线程对同⼀DOM进⾏操作,这样浏览器就会错乱,⽆法判断这两个线程的优先级。为了避免这个问题,JS必须是单线程的语⾔,⽽且在未来这⼀特性也不会改变。因为JS是单线程语⾔,当遇到多个任务时,不可能⼀直等待任务完成,这会造成巨⼤的资源浪费。在了解事件循环机制前,我们要先了解⼀下执⾏栈、任务队列和主线程。执⾏栈所有任务都在主线程上执⾏,形成执⾏栈。当执⾏某个函数、⽤户点击⼀次⿏标、⼀个图⽚加载完成等事件发⽣时,只要指定过回调函数,这些事件发⽣时就会进⼊执⾏栈队列中,等待主线程读取,遵循先进后出原则。任务队列存放异步任务的运⾏结果。只要异步任务有了运⾏结果,就在任务队列中放置⼀个事件。事件循环是唯⼀的,但是任务队列可以有很多个。队列遵循先进先出原则。主线程主线程规定了要执⾏执⾏栈中的哪个事件。主线程会不停的从执⾏栈中读取事件,会执⾏完所有栈中的同步代码,这就是主线程循环。当主线程将执⾏栈中所有的同步任务执⾏完后,主线程将回去查看任务队列是否有任务,如果有,那些对应的异步任务会结束等待状态,进⼊执⾏栈并开始执⾏。⼀、浏览器的事件循环机制JavaScript代码的执⾏过程中,除了依靠函数调⽤栈来搞定函数的执⾏顺序外,还依靠任务队列(task queue)来搞定另外⼀些代码的执⾏。整个执⾏过程,我们称为事件循环过程。⼀个线程中,事件循环是唯⼀的,但是任务队列可以拥有多个。任务队列⼜分为macro-task(宏任务)与micro-task(微任务)宏任务包括:scrip整体代码、setTimeout、setInterval、setImmediate、I/O操作、UI 渲染微任务包括:ck、Promise、Async/Await、MutationObserver顺带⼀提~new Promise是同步任务,e().then()是微任务总结: 先执⾏宏任务,然后执⾏该宏任务产⽣的微任务,若微任务在执⾏过程中产⽣了新的微任务,则继续执⾏微任务,微任务执⾏完毕后,再回到宏任务中进⾏下⼀轮循环。当某个宏任务执⾏完后,会查看是否有微任务队列。如果有,先执⾏微任务队列中的所有任务;如果没有,会读取宏任务中排在最前⾯的任务。执⾏宏任务的过程中,遇到微任务,依次加⼊微任务队列。栈空后再读取微任务队列⾥的任务,以此类推。个⼈理解: 这个事件循环机制我理了很久,先⽤我个⼈的⼤⽩话讲⼀下。对于宏任务和微任务的关系,那就是微任务始终跟在当前宏任务的后⾯,当前宏任务还没执⾏完之前,遇到宏任务先扔⼀边,遇到微任务就跟在当前宏任务后⾯。⾸先JS代码本⾝就是⼀个宏任务,所以在执⾏代码时,JS还没执⾏完之前,遇到宏任务(例如setTimeout)先放⼀边,遇到微任务就跟在当前宏任务后⾯(JS代码),等JS代码全部执⾏完之后,执⾏跟在JS代码后的微任务,当⼀个宏任务和它携带的微任务执⾏完后,就是⼀个事件循环完毕,再去找当前代码中的下⼀个宏任务开始第⼆轮循环。总结来说,就是所有的微任务总会在下⼀个宏任务开始前执⾏完毕。先来看⼀个⼩的例⼦:setTimeout(function () { new Promise(function (resolve, reject) { ('异步宏任务promise'); resolve(); }).then(function () { ('异步微任务then') }) ('异步宏任务');}, 0)new Promise(function (resolve, reject) { ('同步宏任务promise'); resolve();}).then(function () { ('同步微任务then')})('同步宏任务')控制台输出:分析: ⾸先读取整段JS代码(宏任务),遇到第⼀个setTimeout时,判断是⼀个宏任务,先放到⼀边,继续往下执⾏。遇到⼀个newPromise,因为new⼀个对象是⽴即执⾏的,所以new Promise是⼀个同步任务,输出’同步宏任务promise’,接着向下执⾏,遇到then⽅法(微任务),跟在当前宏任务(JS代码)后⾯,继续向下,输出’同步宏任务’。当前宏任务(JS代码)完毕,执⾏跟在宏任务后⾯的微任务(then),输出’同步微任务then‘,此时,⼀个宏任务带着它的微任务执⾏完毕,也就是⼀个事件循环完毕,准备开始第⼆次循环。读取第⼆个宏任务(setTimeout),遇到new Promise,直接执⾏,输出’异步宏任务promise’,然后遇到⼀个微任务(then),跟在当前宏任务(setTimeout)后⾯,继续向下,输出’异步宏任务’,此时当前宏任务执⾏完毕,执⾏跟在它后⾯的微任务(then),输出’异步微任务then’。e().then(()=>{ ('Promise1') setTimeout(()=>{ ('setTimeout2') },0)})setTimeout(()=>{ ('setTimeout1') e().then(()=>{ ('Promise2') })})控制台输出:分析: ⾸先执⾏整段script代码,遇到e().then微任务,跟在当前宏任务(JS代码)之后,继续向下,遇到宏任务(setTimeout1)先放⼀边,执⾏跟在当前宏任务后⾯的微任务,输出Promise1,⼀个事件循环完成。此时产⽣了⼀个宏任务setTimeout2,再次放到⼀边,找到第⼆个宏任务开始执⾏,即setTimeout1,输出setTimeout1,产⽣了⼀个微任务e().then,跟在当前宏任务(setTimeout1)后⾯,宏任务执⾏完后,执⾏微任务,输出Promise2,⼜⼀个事件循环完成。再去找下⼀个宏任务,即setTimeout2,输出setTimeout2。⼆、Node的事件循环机制浏览器中有事件循环,Node中也有,但是与浏览器中的事件循环完全不⼀样。宏任务包括:scrip整体代码、setTimeout、setInterval、setImmediate、I/O操作微任务包括:ck(在微任务队列执⾏之前执⾏)、new Promise().then(回调)采⽤V8作为js的解析引擎,I/O处理使⽤⾃⼰设计的libuv。libuv是基于事件驱动的跨平台抽象层,封装了不同操作系统⼀些底层特性,对外提供统⼀的API,事件循环也是它⾥⾯的 实现。的运⾏机制:V8引擎解析JS脚本解析后的代码,调⽤Node APIlibuv库负责Node API的执⾏。它将不同的任务分配给不同的线程,形成⼀个事件循环,以异步的⽅式将任务的执⾏结果返回给V8引擎。V8引擎再将结果返回给⽤户。libuv引擎中的事件循环分为六个阶段,它们会按照顺序反复执⾏。每当进⼊某⼀个阶段后,都会从对应的回调队列中取出函数去执⾏。当队列为空或者执⾏的回调函数数量到达系统设定的阈值,就会进⼊下⼀阶段。这是⼀张官⽹的node事件循环简化图:⼀共六个阶段,这六个阶段为⼀轮事件循环。我们来看看这⼏个阶段都是做什么的吧:定时器检测阶段(timers): 在这个阶段执⾏timer的回调,即setTimeout、setInterval⾥⾯的回调函数。I/O事件回调阶段(I/O callbacks): 此阶段会执⾏⼏乎所有的回调函数,除了close callbacks(关闭回调)和那些由timers与setImmediate()调度的回调。限制阶段(idel,prepare):仅系统内部使⽤。(内部处理的事务,暂时不需要关注)轮询阶段(poll): 检索新的I/O事件、执⾏与I/O相关的回调,其余情况nide将在适当的时候在此堵塞。检查阶段(check):setImmediate()回调函数在这⾥执⾏。关闭事件回调阶段(close callback):⼀些关闭的回调函数。我们详细了解⼀下poll阶段,这是⼀个⾄关重要的阶段:如果事件循环进⼊了poll阶段,并且代码未设定timer:如果poll 队列不为空,事件循环将同步执⾏队列⾥的callback,直⾄队列为空,或执⾏的callback达到系统上限。如果poll 队列为空:如果代码已经被setImmediate()设定了callback,事件循环将结束poll阶段进⼊check阶段,并执⾏check阶段的队列。如果代码没有设定setImmediate(callback),事件循环将阻塞在该阶段等待callbacks加⼊poll队列,⼀旦到达就⽴即执⾏。如果poll 队列进⼊空状态时(即poll阶段为空闲状态),事件循环将检查timers,如果有⼀个或多个timers事件已经到达,事件循环将按循环顺序进⼊timers阶段,并执⾏timer 队列。接下来看⼀个例⼦便于理解:function someAsyncOperation(callback) { //花费两毫秒 le(__dirname + '/' + __filename, callback);}var timeoutScheduled = ();var fileReadTime = 0;setTimeout(function() { var delay = () - timeoutScheduled; ('setTimeout:' + (delay) + 'ms have passed since I was scheduled');},10);someAsyncOperation(function(){ fileReadTime = (); while(() - fileReadTime < 20) {
}})我们只分析三个⽐较重点的阶段:定时器检测阶段(timers)、I/O事件回调阶段(I/O callbacks)、轮询阶段(poll)⾸先从setTimeout开始,这个定时器是在10ms之后执⾏,所以先将setTimeout的回调添加到I/O中,此时 timer、I/O callbacks 和 poll中还没有任何callback。继续向下执⾏,遇到someAsyncOperation⽅法,这个⽅法读取⼀个⽂件,需要2ms。也就是说2ms之后会有⼀个callback,然后将le回调加⼊到 I/O 中。此时继续进⾏到 poll 阶段,poll 队列为空,且代码没有设定setImmediate(callback),这时事件循环将阻塞在该阶段,并等待callbacks加⼊poll队列。2ms之后,le回调加⼊poll队列,此时⽴即执⾏le的回调,但是le的回调也就是while函数,将会等待20ms才执⾏,也就是说 poll 等待到22ms。但是在10ms的时候setTimeout这个定时器的10ms时间已经到了,但是⽆法执⾏,因为JS是单线程的,setTimeout的回调被阻塞。22ms之后,le的回调执⾏完毕了,此时我们来看这句话:这个时候,setTimeout将会跳过poll阶段,进⾏下⼀次循环来到timer阶段。在timer阶段中,setTimeout的回调被执⾏,代码执⾏结束。三、两者的区别浏览器事件循环机制中,微任务的任务队列是在每个宏任务执⾏完之后执⾏。Node事件循环机制中,微任务会在事件循环的各个阶段之间执⾏,也就是说,⼀个阶段执⾏完毕,就会去执⾏微任务队列的任务。浏览器的事件循环机制:Node的事件循环机制:
发布者:admin,转转请注明出处:http://www.yc00.com/news/1688684405a162373.html
评论列表(0条)