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

js的执行机制

时间:2023-04-03 10:47:51 Node.js

js是在哪里执行的?js的执行引擎基于v8(用c++编写),在chrome和node中都使用。在执行过程中,有以下两部分组成内存堆(内存分配)和调用栈(代码执行)上面两部分的联系是代码在调用栈中执行,会访问到一些对象在执行过程中在内存堆上。我们写的js代码通过js引擎(解释器)转换成高效的机器码。当前的v8引擎由TurboFan和Ignition组成。Ignition是解释器,TurboFan主要是优化代码,提高执行性能。.基于执行引擎的执行原理,我们可以在代码层面做一些优化。可以参考我之前的文章js如何同步执行js。更容易分析堆叠等各种情况。唯一的价值在于js只有一个主线程,栈空间有限。如果递归执行的太深,会出现溢出,所以在写代码的层面需要注意这种情况。为什么会有asynchronousjs异步执行?同步单线程代码易于处理,易于表达,更符合我们的思维方式。为什么还有异步?因为同步会阻塞,在这个高并发的时代,不可能很快。它善于处理海量请求,但同时又不能充分利用硬件资源(cpu和io处理速度的差异你会深有体会)。但是为什么不用多线程,比如java,主要是单线程运行的代码对于多线程来说写起来比较容易,不需要考虑多线程环境下出现的复杂场景,比如死锁等等。异步执行机制?异步执行比较复杂,所以在详细的描述中,关键是在各种使用场景下的执行顺序。这里,需要引入一个概念-->EventLoop。结合下图做一个大概的说明:EventLoop的概念所有的任务都在主线程上执行,形成一个执行上下文栈(executioncontextstack),如上图中的栈区。执行过程中可能会调用异步API,后台线程负责执行具体的异步任务。结束后宏任务回调逻辑放入任务队列,微任务回调逻辑放入微任务队列。主线程执行完毕后,检查微任务队列是否为空。一直执行到队列为空,从macrotask队列中取出一个执行。执行完后,查看并取出执行微任务队列的任务,然后不断重复这个步骤。对于循环过程,一个相应的描述性名词叫做事件循环。Node中异步任务的分类宏任务类型包括脚本整体代码、setTimeout、setInterval、setImmediate、I/O...微任务类型包括Promiseprocess.nextTickObject.observeMutaionObserver...中事件循环各阶段的运行节点如下图所示,上图中的每个方框代表事件循环的一个阶段。每个阶段执行完,或者执行的回调数达到上限,事件循环就会进入下一阶段。timers:达到下限时间后执行定时器setTimeout()和setInterval()设置的回调。I/O回调:除了close回调、timer回调、setImmediate()回调外执行,如操作系统回调tcperror。空闲,准备:仅供内部使用。poll:获取新的I/O事件,如socket读写事件;节点会在合适的条件下阻塞在这里。如果轮询阶段空闲,则进入下一阶段。check:执行setImmediate()设置的回调。关闭回调:执行回调,例如socket.on('close',...)。让我们用一些具体的例子来说明require('fs').readFile('./case1.js',()=>{setTimeout(()=>{console.log('setTimeoutinpollphase');});setImmediate(()=>{console.log('setImmediateinpollphase');});});输出结果为:setImmediateinpollphasesetTimeoutinpollphaseProcessfinishedwithexitcode0,说明setImmediate的回调总是先执行,因为readFile的回调是在poll阶段执行的,所以setImmediate的回调会先执行在poll阶段下一个检查阶段。setTimeout(()=>console.log('setTimeout1'),1000);setTimeout(()=>{console.log('setTimeout2');process.nextTick(()=>console.log('nextTick1'));},0);setTimeout(()=>console.log('setTimeout3'),0);process.nextTick(()=>console.log('nextTick2'));process.nextTick(()=>{process.nextTick(console.log.bind(console,'nextTick3'));});Promise.resolve('xxx').then(()=>{console.log('promise');testPromise();});process.nextTick(()=>console.log('nextTick4'));结果是:nextTick2nextTick4nextTick3promisesetTimeout2setTimeout3nextTick1setTimeout1在描述什么是事件循环时,大致描述了microtask机制,但是它是具体到nextTick的,有一个Tick-Task-Queue专门用来存放process.nextTick的任务,并且有一个调用深度限制,上限为1000。js引擎执行完MacroTask任务后,会先遍历并执行Tick-Task-Queue的所有任务,然后再遍历MicroTaskQueue的所有任务。具体的执行逻辑可以用下面的代码来表达。for(macroTaskofmacroTaskQueue){//1.处理当前的MACRO-TASKhandleMacroTask();//2.处理所有NEXT-TICKfor(nextTickQueue的nextTick){handleNextTick(nextTick);}//3.处理所有MICRO-TASKfor(microTaskQueue的microTask){handleMicroTask(microTask);}}所以会先输出process.nextTick,再输出promise。其他输出序列将不再详细描述。在事件循环机制中已经解释过了。根据上面代码表达的执行逻辑,显然可以得出如下结论。递归调用时会死循环,宏任务不会。testPromise();functiontestPromise(){promise=Promise.resolve('xxx').then(()=>{console.log('promise');testPromise();});}//前面的步骤promise任务换成这个,setTimeout2及之后的输出就再也没有机会出来了。类似于nextTick,这个效果也是一样的。看了一些书,参考了很多资料,理解后输出所学。有时间的话,我们接下来研究一下源码吧。毕竟通过demo验证的结论的说服力不如源码那么直接。参考链接https://jakearchibald.com/201...https://blog.sessionstack.com...https://cnodejs.org/topic/592...https://developer.mozilla.org。..https://nodejs.org/en/docs/gu…