一、前言前几天听某公司做了三年前端说“今天又学了一个知识点——微任务、宏任务”,我问himwhatis,因为吃饭的时候说的比较浅,当时不是很理解,所以私下研究整理了一下,因为说微任务和宏任务就必须说事件循环,于是就有了这个博客。在说事件循环机制之前,我们需要了解一些基础知识:js是单线程的js,一开始作为脚本语言运行在客户端。实际上,js作为脚本语言来操作dom时,是单线程决定的。那么这时候就出现了性能问题,那么js在浏览器端是如何处理这个问题的呢?同时js在后台Node怎么解决呢?这就是本文需要介绍的事件循环机制。这里我从浏览器和Node.js两个方面来分析。2、在讲解浏览器端的事件循环之前,先说一下js中同步代码和异步代码的执行过程。2.1.js同步代码执行过程js引擎会在执行代码的过程中安装sequence存储在某个地方。这个地方叫做执行栈。当我们调用一个方法时,js会生成一个方法对应的方法。相应的上下文(context)。在这个执行环境中,有这个方法的私有作用域,指向上层作用域的指针,方法的参数,这个作用域定义的变量,这个作用域的this对象。functiona(){console.log("methodaexecute...");}functionb(){a();}functionc(){b();}c();用上面的例子来分析:js在执行的过程中会有一个全局上下文,我们这里称之为GContext。下面的分析步骤调用c(),c被压入栈中。此时栈中的内容为:GContext->c-contextC然后调用b(),将b压入栈中。栈,此时栈中的内容为:GContext->c->contextC->b->contextB然后调用a(),将a压入栈中,栈中的内容为:GContext->c->contextC->b->contextB-c->contextCa执行后,a被弹出栈。此时栈的内容为:GContext->c->contextC->b->contextBb被执行,b被弹出栈。此时入栈内容为:GContext->c->contextCc被执行,b出栈。此时栈的内容为:GContext执行完毕,资源释放。以上就是同步代码的执行,其中涉及到两个核心概念:执行整个代码的线程我们称为主线程,方法执行的地方称为执行栈。2.2、js异步代码执行过程上面同步过程说完了,这里说说异步过程。当js引擎遇到异步事件时,不会等待返回结果而是挂起。当异步任务执行时,结果会被添加到不同于执行栈的任务队列中。注意:此时callback并不会在放入队列后立即执行,而是在主线程执行完执行栈中的所有任务后执行。然后去队列中查找是否有任务,如果有则取出第一个事件并将回调放入执行栈并执行其代码。这种重复构成了事件循环。这里还有一个核心概念:taskqueue2.3,microtasks,macrotasks。上面说了,js在执行异步方法的时候,会将返回的结果放到队列中。有两种类型的任务:微任务和宏任务。那么哪些是微任务,哪些是宏任务呢?Microtasks:Promise、process.nextTick()、整体代码脚本、Object.observer、MutationObserverMacrotasks:setTimeout()、setInterval()浏览器执行时,首先从macrotask队列中取出一个macrotask执行宏,然后执行所有宏任务下的微任务,这是一个循环;然后取出执行下一个macrotask,再执行所有的microtasks,这是第二个循环,以此类推。注意:整个javascript代码是第一个宏任务constprocess=require('process')setTimeout(function(){//将宏任务分发到EventQueueconsole.log("1");},0);setTimeout(()=>{console.log("11");},0);setTimeout(()=>{console.log("111");},0);newPromise(function(resolve){console.log('2');resolve();}).then(function(){//发送微任务console.log('3');});//输出231111112.4.总结就是在浏览器端,当我们执行一段脚本时,遇到同步代码,依次进入执行栈,遇到异步代码,挂起,继续执行其他方法。异步方法执行后,根据任务类型进入任务队列。执行完执行栈,主线程空闲下来后,会去任务队列中取任务回调执行。3.在Node端,我觉得Node的事件循环和浏览器端有点不同。它的事件循环依赖于libuv引擎。图片来自官网,展示了节点事件循环的六个阶段。timers:这个阶段执行定时器回调,比如setTimeout()和setInterval()。I/O回调:这个阶段执行除close事件、timer和setImmediate()回调之外的所有回调idle,prepare:内部使用poll:等待新的I/O事件,节点在某些特殊情况下会阻塞在这里check:setImmediate的回调()会在这个阶段执行close回调:比如socket.on('close',...)等close事件的回调,对于我们来说比较关注timer,poll,和这三个阶段检查就是这样。poll阶段主要有两个功能:处理轮询队列(pollqueue)的事件(回调);当达到定时器指定的时间时,执行定时器的回调;poll阶段的逻辑如果事件循环进入poll阶段,代码中没有设置定时器,会发生以下情况:如果轮询队列不为空,事件循环会同步执行队列中的回调,直到队列为空,或者执行的回调达到系统上限;b.如果轮询队列为空,会发生以下情况:*如果代码已经通过setImmediate()设置了回调,事件循环将结束轮询阶段并进入检查阶段,并在检查阶段执行队列(check阶段的队列是通过setImmediate设置的)*如果代码中没有设置setImmediate(callback),事件循环会在这个阶段被阻塞,等待callbacks加入poll队列;如果事件循环进入轮询阶段,代码设置了一个定时器:如果轮询队列进入空状态(即轮询阶段空闲),事件循环将检查定时器,是否有一个或多个定时器到达,事件循环会循环进入timers阶段,执行timerqueue3.1、setTimeout、setImmediate函数。这两个功能还是差不多的,不同的是它们在EventLoop的不同阶段:timer,check。setImmediate(()=>console.log("setInterval"));setTimeout(()=>{console.log("setTimeout")},0);上面两行代码的输出顺序是什么?事实上,有两种可能。1、当setTimeout的0ms不能绝对为0ms时,如果timer阶段已经过去,那么此时会在下一个周期执行setTimeout,也就是说先setInterval,再setTimeout。2、第二种可能是正常流程。先计时,再检查。如果上面的代码后面跟着一个IO操作呢?例如:require('fs').readFile(__filename,()=>{setImmediate(()=>console.log("setInterval"));setTimeout(()=>{console.log("setTimeout")});})这个时候只有一种情况可能,先setInterval,再setTimeout,因为在io中定时器已经执行完毕(在readFile的IO回调中)。我们一起来看下面的代码:setTimeout(()=>{console.log("timer1")Promise.resolve().then(()=>console.log("promise1"));process.nextTick(()=>console.log("nextTick1"))},0);setTimeout(()=>{console.log("timer2")Promise.resolve().then(()=>console.log("promise2"));process.nextTick(()=>console.log("nextTick2"))},0);按照我的理解,它的输出应该是这样的:先定时器,然后在切换阶段的时候执行microtasks//情况1timer1timer2nextTick1nextTick2promise1promise2不是,它的输出一直是与浏览器兼容。于是叫我切换到10.8.0,发现以上两种情况都存在(情况1的比例大于情况2的比例)。其原因尚未确定。3.2.总结node中六个阶段的每一个阶段的执行都会伴随着微任务的执行。同一个MicroTask队列中的Process.tick()会比Promise更好。四、小结本文主要介绍浏览器和Node.js事件循环机制的实现。能力水平有限,不妥之处还望指正。欢迎关注公众号:
