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

请帮我安排一下JS的事件循环!!!

时间:2023-04-03 20:34:57 Node.js

上次大家又和我一起吃喝喝PromiseA+,想必大家脑子里想的一定是西瓜可乐……什么西瓜可乐!明明是承诺!额,醒醒,今天大家搬个小板凳,听我说说JS中比较有趣的事件循环,在了解事件循环之前,先了解几个基本概念。堆栈(Stack)堆栈是一种遵循后进先出(LIFO)的数据集合。新加入或待删除的元素存放在栈尾,称为栈顶,另一端称为栈底。在栈中,新元素靠近栈顶,旧元素靠近栈底。这并不容易理解。让我们举个例子。比如有一个乒乓球箱,我们不停地往球箱里放乒乓球,那么最先放入的乒乓球一定在底部,最后放入的乒乓球一定在顶部。如果我们要取出这些球,是不是一定要按照顺序从上到下取出呢?这个模型是后进先出,也就是我们之后进入球箱的球先出来。栈的概念其实在我们的js中是非常重要的。大家都知道我们的js是单线程语言,那么它的单线程在哪里呢?它在它的主工作线程中,也就是我们常说的执行上下文。这个执行上下文是栈空间,我们看一段代码:console.log('1');函数a(){console.log('2');functionb(){console.log('3')}b()}a()我们知道函数执行的时候会把这个函数放到我们的执行上下文中,函数执行完之后会弹出执行栈被执行了,那么根据这个原理,我们可以知道,这段代码的运行过程是第一个执行的,当我们的代码被执行时,就会有一个全局上下文。这时候代码运行起来,全局上下文执行栈。在堆栈的底部,我们遇到了console.log('1')。该函数在调用时进入执行栈。当这句话执行完,也就是我们执行到下一行的时候,我们的控制台函数就会出栈。这个时候栈里面还是只有全局上下文,然后再运行代码。这里注意,我们遇到的函数声明是不会进入执行栈的,只有当我们的函数被调用执行时,它才会进入。这个原理和我们执行栈的名字完全一样。然后我们遇到了a();这段代码这个时候我们的a函数就进入了执行栈,然后进入到我们函数a的内部,这个时候我们的函数执行栈应该是全局上下文——a然后我运行console.log('2'),然后执行栈变成了全局上下文——a——console,然后我们的控制台跑完了,我们的执行栈恢复到全局上下文——a然后遇到b();然后b进入我们的执行栈,全局上下文-a-b,然后进入b函数内部,执行console.log('3')时,执行栈就是全局上下文-a-b-console,执行完成后返回到全局上下文-a-b然后执行我们的b函数,然后就是弹出执行栈,然后执行栈就变成了全局上下文-a然后我们的a函数就是executed,然后从执行栈中弹出,然后执行栈就变成了全局上下文,我们的全局上下文就会显示在我们的浏览器中,这样我们的执行上下文的执行过程在关闭的时候是怎么弹出的。是不是清楚了很多?通过上面的executioncontext,我们可以发现几个特点:executioncontext是一个单线程的executioncontext,当一个函数被调用的时候会同步执行代码,此时这个函数会进入executioncontext,代码运行会产生一个globalcontext,仅有的当浏览器关闭时,队列(Queue)队列是一个遵循先进先出(FIFO)的数据集合。新的条目将被添加到队列的末尾,而旧的条目将从队列的头部移除。我们可以看到队列和栈的区别在于栈是类似于乒乓球盒的后进先出,而队列是先进先出的,也就是先到的进必先出。我们也举个例子。排队就像我们排队等安检一样。第一个到的人排在队伍的最前面,后来来的人排在队伍的后面。一个人完成后释放一个人。这样的团队是不是一个先进先出的过程。Queue这里要提到两个概念,宏任务(macrotask)和微任务(microtask)。任务队列Js的事件执行分为宏任务和微任务。宏任务主要由script(全局任务)、setTimeout、setInterval、setImmediate、I/O组成,UI渲染微任务主要是process.nextTick、Promise.then、Object。观察者,MutationObserver。浏览器事件循环js在执行代码的时候,如果遇到上面的任务代码,会先把这些代码的回调放到对应的任务队列中,然后继续执行主线程的代码,直到执行上下文结束函数执行完毕,会先去微任务队列执行相关任务。microtask队列清空后,会从macrotask队列中取出任务放入执行上下文,然后继续循环。执行代码,遇到宏仁,放入宏仁队列,遇到微任务,放入微任务队列,执行其他函数时,放入执行上下文,然后从宏仁队列中取出第一项放入执行上下文中执行,然后继续循环1-3的步骤。这就是浏览器环境下的js事件循环。//学完上面的面试题我们来看看事件循环setTimeout(function(){console.log(1);},0);Promise.resolve(function(){console.log(2);})newPromise(function(resolve){console.log(3);});控制台日志(4);//上面代码的输出是什么???想,想,想,想~~~

正确答案是341,是不是一样如你想象的?看一下代码的运行过程//遇到setTimeout时,将setTimeout回调放入宏仁队列setTimeout(function(){console.log(1);},0);//当遇到promise,但是没有then方法回调,所以这段代码在执行的时候会进入我们当前的执行上下文,然后出栈Promise.resolve(function(){console.log(2);})//遇到了一个新的Promise,不知道大家还记得我们在上一篇文章中提到,Promise有一个原理,当Promise初始化的时候,Promise里面的构造函数会立即执行,所以a3会这里会立即输出,所以这个3是第一个输入newPromise(function(resolve){console.log(3);});//然后进入第二个输出4代码执行时,回到microtask队列中查看是否有task,发现microtask队列为空,然后去宏仁队列中查找,发现有一个我们刚刚放入的setTimeout回调函数,然后取出这个task执行,于是马上输出1条console.log(4);看到上面的解释,是不是都明白了,直接调用是不是很简单呢~那我们再来看看node环境下的事件执行循环。NodeJs事件循环浏览器的EventLoop遵循HTML5标准,而NodeJsTheEventLoop遵循libuv标准,所以在事件的执行上会有一些差异。大家都知道nodejs其实就是js的一个runtime,也就是运行环境。在这种环境下,nodejs的API很大。其中一些是通过回调函数和事件发布订阅方式执行的。那么我们的代码在这样的环境下的执行顺序是怎样的呢,也就是我们不同的回调函数是怎么分类的,然后根据什么分类的呢?执行顺序其实是由我们的libuv决定的┌──────────────────────────┐┌──>│计时器││└──────────────┬──────────────┘│┌────────────┴──────────────┐││等待回调││└──────────────┬────────────┘│┌────────────┴──────────────┐││空闲,准备││└──────────────┬──────────────┘┌──────────────────┐│┌──────────────┴──────────────┐│传入:│││投票│<──────┤连接,││└────────────┬──────────────┬────────────┘│数据等││┌─────────────┴──────────────┐└────────────────┘││检查││└──────────────┬────────────┘│┌──────────────┴──────────────┐└──┤关闭回调│└──────────────────────────────┘来看看这六个任务是用来做什么的:在这个阶段执行settimeout()和setInterval())callbackpendingcallbacks:上一个周期的少量I/O回调会延迟到本周期的这个阶段。空闲,准备:仅供内部使用。poll:执行I/O回调,在合适的条件下会被阻塞。check:执行setImmediate()设置的回调。关闭回调:执行回调,例如socket.on('close',...)。我们来看一张网上找的nodejs执行图。我们可以看到图中有六个步骤。当代码执行的时候,如果我们在这六个步骤中遇到回调函数,就会被放入对应的队列中,然后当我们执行完同步字符的时候,我们就会切换到下一个阶段,也就是定时器阶段.然后,在timer阶段的执行过程中,这个阶段的所有回调函数都会执行完,然后进入下一阶段。需要注意的是,我们在每切换一个stage时,都会先执行microtask队列中的所有task,然后进入下一个taskstage,所以我们可以得出nodejs事件循环序列同步代码执行和清除微任务队列。执行timer阶段的所有回调函数(即setTimeout、setInterval),清空microtask队列,执行pendingcallbacks阶段的所有回调函数,清空microtask队列,执行idle,prepare阶段的回调函数.清空微任务队列,执行poll阶段的所有回调函数,清空微任务队列,执行check阶段的回调函数(即setImmediate),清空微任务队列,执行close回调阶段的回调函数,然后是循环1-6阶段,我们来练练手~~~//看看我们的执行阶段letfs=require('fs');//遇到setTimeout,放到定时器回调中setTimeout(function(){Promise.resolve().then(()=>{console.log('then1');})},0);//放入微任务队列Promise.resolve().then(()=>{console.log('then2');});//将i/o操作放入挂起的回调回调fs.readFile('./text.md',function(){//将其放入检查阶段setImmediate(()=>{console.log('setImmediate')});//放进入微任务队列process.nextTick(function(){console.log('nextTick')})});首先,同步代码执行完后,我们先清空microtask,然后输出then2,然后切换到timer阶段,执行timer回调,输出then1,然后执行i/o操作回调,然后清空microtask队列,输出nextTick,然后进入check阶段,清除check阶段回调输出setImmediate所有的规则看似云里雾里,但是只要我们总结一下如果我们了解了规则及其运行机制,那么我们就掌握了这些规则,好吧,今天学了这么多,废话少说,我们去写业务代码吧…………