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

泡杯茶,坐下来聊一聊Javascript的事件环

时间:2023-04-03 16:27:20 Node.js

栈和队列在计算机内存中存储和检索数据。基本数据结构分为栈和队列。栈(Stack)是一种后进先出的数据结构。注意,有时栈也被称为“栈”,但“堆”是另一种复杂的数据结构,它与栈完全不同。栈的特点是只在一端进行操作。一般来说,对栈的操作只有两种:push和pop。最先入栈的数据总是最后出来的。队列(Queue)类似于栈,但它是一种先进先出的数据结构。插入数据的操作是从队列的一端进行的,而删除操作是在队列的另一端进行的。一个流行的比喻是,堆栈就像一个固定的桶。先入栈的数据会放在桶底。出栈时,数据会在桶口一个一个的取出,所以先入栈的数据永远是最后一个。被取出来。队列就像一根水管。先进入队列的数据会先从队列的另一端流出。这是他们之间最大的区别。在javascript中,一个函数的执行就是一个典型的push和pop的过程:functionfun1(){functionfun2(){functionfun3(){console.log('doit');}fun3();}fun2();}fun1();程序执行时,fun1、fun2、fun3先依次入栈,调用函数时,先调用(出栈)fun3,再调用fun2、fun1。试想一下,如果fun1先pop出来,fun2和fun3这两个函数就会丢失。单线程与异步在javascript语言中,程序是单线程的,只有一个主线程。为什么是这样?因为不难想象,javascript最初的设计是一种运行在浏览器中的脚本语言。如果设计成多线程,两个线程同时修改DOM,谁占上风?因此,javascript是单线程的。在一个线程中,代码会一句一句往下执行,直到程序运行完毕。如果中间有耗时的操作,那就只能等待了。单线程设计使得语言的执行效率很差。为了利用多核CPU的性能,javascript语言支持异步代码。当有耗时操作时,任务可以写成异步执行。当一个异步任务还没有执行完时,主线程会挂起这个异步任务,继续执行后面的同步代码,如果有异步任务已经运行完,再回头执行。这种执行代码的方式其实非常适合我们生活中的很多场景。例如,小明下班回家。他口渴极了,想烧水泡茶。如果同步执行,就是烧水。小明傻等,等水开再泡茶;如果是异步执行,小明会先开始烧水,然后去做其他事情,比如看电视,听音乐,等水烧开再泡茶。去泡茶。显然第二种异步方式效率更高。常见的异步操作有哪些?有很多,我们可以列举几个常见的:AjaxDOM的事件操作setTimeoutPromise的then方法Node的读取文件先来看一段代码://示例1console.log(1);setTimeout(function(){console.log(2);},1000);控制台日志(3);这段代码很简单,放到浏览器中执行,结果如下:132因为setTimeout函数延迟了1000毫秒,所以先输出1和3,1000毫秒后输出2,非常符合逻辑。我们稍微改动一下代码,将setTimeout的延迟时间改为0://Example2console.log(1);setTimeout(function(){console.log(2);},0);//0毫秒,无延迟console.log(3);运行结果:132为什么延迟0毫秒还是最后输出2?别急,我们再看一段代码://例3console.log(1);setTimeout(function(){console.log(2);},0);Promise.resolve().then(function(){console.log(3);});console.log(4);运行结果:1432以上三段代码,如果你能正确写出结果并说明为什么会这样输出,说明那你对javascript事件循环不熟悉了解的很清楚。说不出来,就说说这里发生的事情吧。这其实很有趣。javascript是如何执行的?开头先简单说一下基本的数据结构。跟我们现在说的事件循环有关系吗?当然有,首先要明确的是javascript代码的执行都是在栈上的,无论是同步代码还是异步代码,这个都要清楚。代码大致分为同步代码和异步代码。实际上,异步代码可以进一步分为两大类:宏任务和微任务。不管是什么宏观任务和微观任务,往往这样高深的术语并不利于我们的理解。我们先这么想:macro就是宏观,大;micro的意思是微型和小。JavaScript是一种解释型语言,它的执行过程是这样的:从上到下解释每一个js语句。如果是同步任务,则入栈(主线程);如果是异步任务,则放入任务队列。开始执行栈中的同步任务,直到栈中的所有任务执行完毕。这时候栈就清空了。回过头来看,如果异步队列中有异步任务完成了,就会产生一个事件,注册一个回调,压入栈中,然后回到第3步,直到异步队列清空,程序就完成了跑完了。很难用语言来描述。还是看图比较好:通过上面的步骤可以看出,无论是同步还是异步,只要是执行的,就一定是在栈上执行,并一遍又一遍地检查异步队列,这种执行方式就是所谓的“事件循环”。了解了javascript的执行原理之后,我们就不难理解之前的第二段代码了。为什么setTimeout为0的时候会最后执行,因为setTimeout是异步代码,异步队列要等所有同步代码都执行完才会执行。.无论setTimeout执行多快,它也不可能在同步代码之前执行。浏览器中的事件我们讲了那么多,宏任务和微任务好像都没有讲过。上面说了,异步任务分为微任务和宏任务。他们使用什么样的执行机制?注意!微任务和宏任务在浏览器和Node中的执行方式是有区别的,有区别!重要的事情再说几遍,下面讨论的是浏览器环境。在浏览器的执行环境中,总是先执行小任务和微任务,然后再执行大任务和宏任务。回过头来看第三段代码。为什么Promise的then方法会在setTimeout之前执行?基本原理是Promise的then方法是一个微任务,setTimeout是一个宏任务。接下来我们借用阮一峰老师的一张图来说明一下:其实我们可以在上图中再细化一点。这张图只画了一个异步队列,也就是说没有区分微任务队列和宏。任务队列。我们可以下定决心,在这张图上加一个microtask队列,在javascript执行的时候再加一个判断,如果是microtask就加到microtask队列中,加一个macrotask到macrotask队列中,清空队列,浏览器总是会优先清空“微任务”。这样,浏览器的事件循环就完全解释清楚了。最后来个大测试,下面代码的结果是什么:将这段代码复制到chrome中运行,结果为:54123下面尝试分析一下为什么会出现这个结果,先输出5,因为console.log(5)是同步代码,对此无话可说。然后将前两个setTimeout和最后一个Promise放入异步队列,注意区分。这时执行同步代码后,发现microtask和macrotask队列中都有代码。根据浏览器的事件环机制,微任务先执行,此时输出4。然后执行宏任务队列中的第一个setTimeout,输出1。此时setTimeout中还有一个Promise,被放入微任务队列中。再次清空微任务队列,输出2。最后宏任务队列中有最后一个setTimeout,输出3。Node中的事件循环和Node中的事件循环与浏览器略有不同。在node.js的官方文档中有具体描述。文档中有一张图详细解释了它的事件循环机制。拿出来:可以看到,node.js中的事件循环机制分为6个阶段,上面我把最重要的3个阶段都标出来了:timer阶段指的是宏任务轮询,比如setTimeout,在轮询中阶段,比如读取文件等宏任务的检查阶段,setImmediate宏任务图中的每个阶段代表一个宏任务队列。在Node事件环中,微任务的运行时间在每个“宏任务队列”清零后,在进入下一个宏任务队列前执行。这是与浏览器最大的区别。说代码吧,有个经典的Node.js事件循环面试题:constfs=require('fs');fs.readFile('./1.txt',(err,data)=>{setTimeout(()=>{console.log('timeout');});setImmediate(()=>{console.log('immediate');});Promise.resolve().then(()=>{console.日志('承诺');});});运行结果:Promiseimmediatetimeout代码并不复杂,首先使用fs模块读取一个文件,回调里面有两个macrotasks和一个microtasks,microtasks的执行总数要好于macrotasks的执行,所以先输出Promise。但是为什么输出后的差异首先出现?原因是fs读取文件的宏任务在上图中的第四轮询阶段。第四阶段队列清空后,应该进入第五检查阶段,也就是setImmediate这个宏任务所在的阶段。与其跳回第一阶段,不如先输出immedate。Tail总结道,分析完浏览器和Node的事件循环,发现它们并不容易,但只要记住它们之间的区别,就可以分析出结果。浏览器事件循环是在运行一个宏任务后立即清除微任务队列。Node事件环在清除一个stage的macrotask队列后,再清除microtask队列。最后总结一下常用的宏任务和微任务:然后是宏任务微任务setTimeoutPromise的方法setIntervalprocess.nextTicksetImmediateMutationObserverMessageChannel