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

JavaScript运行机制--EventLoop详解

时间:2023-04-03 17:18:36 Node.js

JavaScript(简称JS)是前端的主要研究语言。要想真正理解JavaScript,就绕不开它的运行机制——EventLoop(事件循环)JS是一门单线程的语言,异步操作是实际应用中的重要一环。异步操作可以参考我的另一篇文章《js异步发展史与Promise原理解析》,这里不再赘述。堆、栈、队列堆(heap)是指程序运行时申请的动态内存,用于JS运行时存放对象。堆栈(stack)堆栈(stack)遵循的原则是“先进后出”。JS的基本数据类型和指向对象的地址存放在栈内存中。另外,还有一个用于执行JS主线程的栈内存——executeStack(执行上下文栈),本文中的栈只考虑执行栈。队列(queue)队列(queue)遵循的原则是“先进先出”。除了主线程之外,JS中还有一个“任务队列”(其实有两个,后面会详细介绍)。EventLoopJS的单线程意味着所有的任务都需要按照一定的规则排队执行。这个规则就是我们要讲解的EventLoop事件环。EventLoop在不同的运行环境下有不同的方法。浏览器环境下的EventLoop首先如上图所示(转自PhilipRoberts的演讲《Help, I'm stuck in an event-loop》)。当主线程运行时,JS会产生一个堆和栈(执行栈)。主线程中调用的webaip产生的异步操作(dom事件,ajax回调,定时器等)只要产生结果,回调就被塞进“任务队列”执行。当主线程中的同步任务执行完毕后,系统会依次读取“任务队列”中的任务,并将任务放入执行栈中执行。在执行任务时,可能会产生新的异步操作,会产生新的循环,整个过程无穷无尽。从事件循环中不难看出,当我们调用setTimeout并设置了某个时间后,这个任务的实际执行时间可能会大于我们设置的时间,因为主线程中的任务还没有执行完,导致不准确的定时器,也是连续调用setTimeout和调用setInterval会有不同效果的原因(这里不展开,有时间会单独写一篇文章)。下一段代码:console.log(1);console.log(2);setTimeout(function(){console.log(3)setTimeout(function(){console.log(6);})},0)setTimeout(function(){console.log(4);setTimeout(function(){console.log(7);})},0)console.log(5)代码中setTimeout的时间为0,相当于4ms,也有可能大于4ms(不重要)。我们要注意的是代码输出的顺序。我们以任务的输出编号命名任务。最先执行的一定是同步代码,先输出1、2、5、3个任务,然后4个任务依次进入“任务队列”。同步代码执行完后,队列中的3会进入执行栈执行,4会到达队列的最前面。3执行完后,内部setTimeout会把6的任务放到队列尾部。开始执行4个任务...最后我们得到的输出是1,2,5,3,4,6,7。宏任务和微任务任务队列中的任务会不会都乖乖排队呢?答案是否定的,任务也不一样,总有一些任务是有一些特权的(比如插队),也就是任务中的vip——微任务(micro-task),没有特权的——宏——任务(宏任务)任务)。我们来看一段代码:console.log(1);setTimeout(function(){console.log(2);Promise.resolve(1).then(function(){console.log('promise')})})setTimeout(function(){console.log(3);})根据“排队论”,结果应该是1、2、3,promise。但是实际结果适得其反,输出的是1、2、promise、3。明明是3先进入的队列,为什么promise输出在前面呢?这是因为promise有成为微任务的特权。执行主线程任务时,无论是否在后,都会先执行微任务,然后再执行宏任务。也就是说,其实有两个任务队列,一个是宏任务队列,一个是微任务队列。当主线程执行完毕,如果微任务队列中有微任务,则先进入执行栈。当微任务队列中没有任务时,就会执行宏任务的队列。Microtasks包括:nativePromise(一些实现的promise将then方法放在macrotasks中),Object.observe(废弃),MutationObserver,MessageChannel;宏任务包括:setTimeout、setInterval、setImmediate、I/O;Node环境下EventLoop┌────────────────────────────────────┐┌─>│定时器││└─────────────┬──────────────┘│┌────────────┴────────────────┐┐│I/O回调││└────────────┬────────────────┘│┌──────────────────────────────────────────────────────┐││idle,prepare...────────┴────────────┐│incoming:│││poll│<──────┤connections,││└──────────────────────────────────┘│数据,等。││┌────────────────────┐────────────────────────┘││检查││└──────────┬──────────────┘│┌────────────┴──────────────┐└──┤关闭回调│└────────────────────────┘时间循环node中与浏览器中的不同,如图:timers阶段:该阶段执行setTimeout(callback)和setInterval(callback)调度的回调;I/O回调阶段:执行除关闭事件、定时器(timer、setTimeout、setInterval等)和除setImmediate()设置的回调之外的回调;空闲、准备阶段:仅供节点内部使用;poll阶段:获取新的I/O事件,节点在合适的情况下会阻塞在这里;check阶段:执行setImmediate()设置的回调;closecallbacks阶段:比如socket.on('close',callback)的回调会在这个阶段执行。每个阶段都有一个带回调的先进先出队列(queue),当事件循环运行到指定阶段时,节点会执行该阶段的先进先出队列(queue)。当队列回调执行完毕或执行回调次数超过该阶段上限时,事件循环将转入下一阶段。process.nextTickprocess.nextTick方法不在上面的事件循环中,我们可以把它理解为一个微任务,它的执行时机是当前“执行栈”的尾部——下一个EventLoop(主线程读取“任务”queue"")before----触发回调函数。即它指定的任务总是发生在所有异步任务之前。setImmediate方法在当前“任务队列”的末尾添加一个事件,即任务它指定总是在下一个事件循环中执行。上面的代码:process.nextTick(functionA(){console.log(1);process.nextTick(functionB(){console.log(2);});});setTimeout(functiontimeout(){console.log('TIMEOUTFIRED');},0)//1//2//TIMEOUTFIRED代码表明在指定的回调函数超时前不仅执行了函数AsetTimeout,也是函数B在超时前执行,这意味着如果有多个process.nextTick语句(无论嵌套与否),它们都会在当前“执行堆栈”上执行。setTimeout和setImmediate非常相似,但两者之间的区别取决于它们何时被调用。setImmediate被设计为在poll阶段完成时执行,即check阶段;setTimeout被设计为在轮询阶段空闲并达到设定时间时执行;但是,它们在定时器阶段的执行顺序取决于当前事件循环的上下文。如果在异步i/o回调之外调用它们,则它们的执行顺序是不确定的。setTimeout(functiontimeout(){console.log('timeout');},0);setImmediate(functionimmediate(){console.log('immediate');});$节点timeout_vs_immediate.jstimeoutimmediate$节点timeout_vs_immediate.jsimmediatetimeout这是因为当后面的事件进入时,事件循环可能处于不同的阶段,导致不确定的结果。当我们给事件循环一个特定的上下文时,事件的顺序就可以确定了。varfs=require('fs')fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout')},0)setImmediate(()=>{console.log('immediate')})})$nodetimeout_vs_immediate.jsimmediatetimeout这是因为执行fs.readFile回调后,程序设置了timer和setImmediate,所以poll阶段不会阻塞,进入check阶段。先执行setImmediate,然后进入定时器阶段执行setTimeout。