浏览器中的事件轮询原著JavaScript是一种单线程语言。之所以叫单线程,是因为在浏览器中,如果是多线程,如果两个线程同时操作同一个Dom元素,最后的结果就会有问题。所以,JavaScript是单线程的,但是如果代码是从上到下逐行执行的,如果一个代码块执行时间长,必须等待当前执行完成,效率很低,所以有准确的说JavaScript的主线程是单线程的,但是也有其他的线程来帮助我们实现异步操作,比如定时器线程,事件线程,Ajax线程等。在浏览器中有两个区域可以执行JavaScript。一种是我们通常所说的同步代码执行,在栈上执行。原则是先进后出。在执行异步代码时,分为两个队列,macro-task(宏任务)和micro-task(微任务),遵循先进先出的原则。//作用域链函数one(){console.log(1);功能二(){console.log(2);功能三(){console.log(3);}三();}two();}one();//1//2//3以上代码均为同步代码。执行时,先将全局作用域入栈,执行全局作用域内的代码,解析函数一,当执行函数调用one()时,将一的作用域入栈,在一执行代码,打印1,解析二,执行two(),将2压入堆栈,执行2,打印2,解析3,执行three(),将3压入堆栈,执行3,打印3。在执行完释放函数的过程中,因为一个是在全局范围内执行,二是在一个执行,三是在两个执行,所以在释放内存的时候,必须从内层释放到外层,而三执行完就释放了,此时三不再占用二的执行环境,释放二,二不再占用一的执行环境,释放一,一不再占用全局作用域的执行环境,最后释放全局作用域,在栈中执行同步代码时的先进后出原则更像是一个杯子,先放进去的在最下面,需要最后取出。异步队列更像是一个管道,有两个端口,从入口进入,从出口退出,所以是先进先出。宏任务队列中有setTimeout、setInterval、setImmediate、MessageChannel,微任务用Promise表示。然后方法,MutationObserve(不推荐)。案例1letmessageChannel=newMessageChannel();letprot2=messageChannel.port2;messageChannel.port1.postMessage("Iloveyou");console.log(1);prot2.onmessage=function(e){console.log(e.data);};console.log(2);//1//2//Iloveyou从上面的案例可以看出,MessageChannel是一个宏任务,比同步代码晚执行。情况2setTimeout(()=>console.log(1),2000);setTimeout(()=>console.log(2),1000);控制台日志(3);//3//2//1上面的代码可以看出,setTimeout在执行同步代码的时候并没有放入异步队列,而是在等待时间到来的时候放入异步队列,所以上面的结果将获得。Case3setImmediate(function(){console.log("setImmediate");});setTimeout(function(){console.log("setTimeout");},0);console.log(1);//1//setTimeout//setImmediate也是一个宏任务。当setTimeout的延迟时间为0时,setImmediate比setTimeout更晚的被放入异步队列。这里需要注意的是setImmediate是在浏览器端的,目前只有IE实现了。以上案例都是关于宏任务的。下面以微任务为例,看看微任务和宏任务的执行机制。微任务在浏览器端的代表其实就是Promise的then方法。案例4setTimeout(()=>{console.log("setTimeout1");Promise.resolve().then(data=>{console.log("Promise1");});},0);Promise.resolve().then(data=>{console.log("Promise2");setTimeout(()=>{console.log("setTimeout2");},0);});//Promise2//setTimeout1//Promise1//setTimeout2从上面的执行结果我们其实可以看出,在栈中执行完同步代码之后,会先执行微任务队列,微任务队列执行完之后,macrotaskqueue会被执行,macrotask队列执行完一个macrotask后,会检查是否有新的microtasks。如果是,则在执行下一个macrotask之前清空microtask队列,依次轮询,直到清空整个异步队列。Node中的事件轮询Node中的事件轮询机制与浏览器类似但又有所不同。相似的是同步代码也是先入栈执行,也是先进后出。不同的是,Node有自己的多个处理不同问题的阶段,对应的队列也有自己内部实现的microtaskprocess.nextTick。Node的整个事件轮询机制都是由Libuv库实现的。Node中事件轮询的流程如下:从图中可以看出,Node中有多个队列,分别执行不同的操作,每次队列切换时,microtask队列执行一次,循环往复查询。案例1setTimeout(function(){console.log("setTimeout");},0);setImmediate(function(){console.log("setInmediate");});默认setTimeout和setImmediate不知道哪个先执行,顺序不固定,Node执行时有准备时间,setTimeout延迟时间设置为0其实是4ms左右,假设Node准备时间在4ms以内,开始轮询,定时器还没有超时,于是轮询到下一个队列。此时需要再次循环到定时器队列后执行定时器,所以会先执行校验队列的setImmediate。如果Node执行的准备时间大于4ms,则定时器队列会先执行,因为定时器的回调在同步代码执行完后已经放入定时器队列中。案例2setTimeout(()=>{console.log("setTimeout1");Promise.resolve().then(()=>{console.log("Promise1");});},0);setTimeout(()=>{console.log("setTimeout2");},0);console.log(1);//1//setTimeout1//setTimeout2//Promise1Node事件轮询,轮询每个队列时,轮询当前队列之后task清空,microtask队列清空一次再切换到下一个队列,这点和浏览器端不同。浏览器会执行宏任务队列中的一个任务,并将其插入到执行微任务队列中。清空微任务队列后,会返回宏任务队列执行下一个宏任务。在上面的例子中,在Node事件轮询中,定时器队列清空后,microtask队列被执行,然后轮询下一个队列。案例3setTimeout(()=>{console.log("setTimeout1");},0);setTimeout(()=>{console.log("setTimeout2");},0);Promise.resolve().then(()=>{console.log("Promise1");});console.log(1);//1//Promise1//setTimeout1//setTimeout2上面代码的执行过程就是栈执行首先,执行堆栈时打印1,Promise.resolve()生成微任务。stack执行完后,从stack切换到timerqueue之前,先执行microtaskqueue,再执行timerqueue。案例4setImmediate(()=>{console.log("setImmediate1");setTimeout(()=>{console.log("setTimeout1");},0);});setTimeout(()=>{console.log("setTimeout2");setImmediate(()=>{console.log("setImmediate2");});},0);//结果1//setImmediate1//setTimeout2//setTimeout1//setImmediate2//结果2//setTimeout2//setImmediate1//setImmediate2//setTimeout1setImmediate和setTimeout的执行顺序不固定。假设先执行checkqueue,会执行setImmediate打印setImmediate1,遇到的timer会被放入timerqueue,轮询到timerqueue,因为执行栈中的同步代码已经在timer中放了一个timer队列,所以依次执行两个setTimeout,执行第一个定时器打印setTimeout2,将遇到的setImmediate放入check队列,执行第二个定时设备打印setTimeout1,再次轮询check队列执行新加入的setImmediate,打印setImmediate2,并产生结果1。假设定时器队列先执行,会执行setTimeout打印setTimeout2,将遇到的setImmediate放入check队列,轮询到check队列,因为栈中执行的同步代码已经在check队列中放入了一个setImmediate,所以会依次执行两次setImmediate,执行第一次setImmediate打印setImmediate1,将遇到的setTimeout放入定时器队列,执行第二次setImmediate打印setImmediate2,再次轮询定时器队列执行新加入的setTimeout,打印setTimeout1,生成结果2.Case5setImmediate(()=>{console.log("setImmediate1");setTimeout(()=>{console.log("setTimeout1");},0);});setTimeout(()=>{process.nextTick(()=>console.log("nextTick"));console.log("setTimeout2");setImmediate(()=>{console.log("setImmediate2");});},0);//Result1//setImmediate1//setTimeout2//setTimeout1//nextTick//setImmediate2//Result2//setTimeout2//nextTick//setImmediate1//setImmediate2//setTimeout1这个和上面的情况类似,不同的是,在执行setTimeout时会产生一个microtasknextTick,我们只需要知道在Node事件轮询中,切换队列时必须先执行microtask队列。先执行检查队列还是先执行定时器队列,就会很容易分析出上面的两个结果。案例6constfs=require("fs");fs.readFile("./.gitignore","utf8",function(){setTimeout(()=>{console.log("timeout");},0);setImmediate(function(){console.log("setImmediate");});});//setImmediate//timeout上例中setTimeout和setImmediate的执行顺序是固定的,前面的不固定。这就是为什么?因为上面不是固定的,在栈上执行同步代码的时候会遇到setTimeout和setImmediate,因为无法判断Node的准备时间,不确定准备结束定时器是否超时加入定时器队列.从上面的代码可以清楚的看出,Node准备好之后,会直接执行poll队列来读取文件。在回调中,将setTimeout和setImmediate分别添加到定时器队列和检查队列中。节点队列的轮询是顺序的。在poll队列之后,应该先切换到check队列,然后再重新poll到timer队列,这样就得到了上面的结果。案例7Promise.resolve().then(()=>console.log("Promise"));process.nextTick(()=>console.log("nextTick"));//nextTick//Node中的Promise有两个微任务,Promise的then方法和process.nextTick。从上面案例的结果可以看出,process.nextTick在微任务队列中最先执行。以上内容就是浏览器和Node之间的事件轮询规则。相信看完之后,你应该已经深入了解了浏览器和Node的事件轮询机制,深刻体会到它们之间的异同。不同的。
