看标题,我想大家应该猜到我要说什么了,在我在谈论这个话题之前首先需要先说明一点:接下来的描述可能会存在错误,和许多我自己的观点。如有不对的地方还请大家帮忙指正。
说到event loop,首先先来说一说JavaScript的运行机制。
JavaScript的运行机制
JavaScript是一门主线程是单线程的语言。为什么会这样规定呢?因为如果多个线程同时做一件事那岂不很混乱,比如一个线程要删除a,另一个线程在同样时间要修改a,那应该执行谁呢?为此,为了简化操作,JavaScript的主线程设置为单线程。
这样不难猜出JavaScript在运行代码的时候是自上而下的运行代码,当我写入一段代码时,执行的顺序应该是先后顺序的。看一段代码:
//javascript
console.log(1);
setTimeout(function(){
console.log(2);
Promise.resolve().then(function(){
console.log(5);
})
},0);
Promise.resolve().then(function(){
console.log(3)
})
console.log(4);
按照我们刚刚推理的,输出的结果顺序应该是1->2->3->4。然而,并不是。那这是为什么呢?这一段简简单单的代码就带来了很多讯息。
这段代码遵循的是同步代码先执行,异步代码后执行,且代码都会放入栈中执行。这个地方就出现了一些专业术语,什么是同步?什么是异步?什么是栈呢?
堆和栈
JavaScript中有栈和堆。比较片面理解的话,其实可以这样说,堆就是用来存储复杂的,比如对象(Object),而栈可以叫执行栈,用来执行代码的。栈(stack) 是自动分配内存空间,它由系统自动释放; 堆(heap) 则是动态分配的内存,大小不定也不会自动释放,堆里面存放的是对象或者数组对象。
同步和异步
其实在理解同步和异步这俩词的时候我觉得举例子更能说清楚一些。举例:我烧开水从热水一直等到水烧开,中途没有做其他任何事,只是等水烧开。这个过程就是同步。如果我在水还没烧开的同时,把水壶洗干净,里面倒上茶,水烧开后将开水倒入水壶中。这个过程就是异步。
同步: 指的是被调用者在执行任务等到完成后返回结果
异步: 指的是被调用者在执行任务时,过一段时间再返回结果。
浏览器的event loop
其实上述代码中,遵循的是一个不断循环的过程(如下图所示),在这个循环中,所有的代码将会在栈中执行,每一次循环都会将宏任务依次排列到 队列(queue) 中(比如:setTimeout),并检查是否有微任务,如果有,先将微任务的代码执行,在执行宏任务中的代码。

因此我们再来重新看上面的代码:同步先输出来就有1,4,紧接着到microtask里面的promise.then(),输出3,最后输出macrotask里的setTimeout()的值2,顺序就是1->4->3->2。
node.js的Event Loop
node.js简介
node.js是基于Chrome v8引擎的JavaScript运行环境,使用了事件驱动,非阻塞I/O的模型。node.js的特点是异步且主线程也是单线程。
node.js的优点:
占用资源小,因为是单线程,在大负荷情况下,对内存占用仍然很低;
线程安全,没有加锁、解锁、死锁这些问题。
node.js Event Loop
node.js的底层也是多个线程,阻塞操作封装的。node.js里由6个阶段来执行event loop的,可以查阅资料点这里,这里就不copy了。现在我们主要叙述node的event loop与浏览器的event loop的区别。
浏览器与node.js两者的event loop
根据两者的event loop的执行机制,他们在运行代码的执行顺序是存在差异的。 来看一个例子:
//javascript
console.log(1);
setTimeout(function(){
console.log('setTimeout1');
process.nextTick(function(){
console.log('nextTick1');
})
},0);
process.nextTick(function(){
console.log('nextTick2');
setTimeout(function(){
console.log(setTimeout2);
})
})
console.log(2);
浏览器执行的过程
在浏览器中运行process的代码,需要在服务器上运,process属于node.js的语法。
1.第一遍执行,从上往下走,遇到同步代码,输出1;遇到setTimeout,排入队列第一个;再往下走,遇到nextTick(),排入微任务第一个,再往下走,遇到同步代码,输出2。第一遍循环:输出1->2 。
2.第二遍执行,取出第一遍循环中第一次排入微任务中的nextTick,输出nextTick2,往下执行时,发现还有setTimeout,将其排入队列中。微任务走完,到宏任务代码,输出setTimeout1,往下走时,发现一个process.nextTick(),放入微任务中。第二遍循环:输出nextTick2->setTimeout1
3.第三遍执行,只剩下微任务中一个process.nextTick(function(){console.log('nextTick1')})和队列中的setTimeout(function(){console.log('setTimeout2')}),先执行微任务,输出promise2,再执行宏任务,输出setTimeout1。第三遍循环:输出nextTick1->setTimeout2
最后输出的顺序应该是:1->2->nextTick2->setTimeout1->nextTick1->setTimeout2
node.js执行的过程
node.js执行的过程遵循它的六个阶段,如图(图片是借鉴的):
每个阶段都有自己的callback队列,每当进入某个阶段,都会从所属的队列中取出callback来执行,当队列为空或者被执行callback的数量达到系统的最大数量时,进入下一阶段。这六个阶段都执行完毕称为一轮循环。

我们再来看上面那段代码的执行顺序。
第一遍执行:代码自上而下执行,遇到同步代码,先输出1,发现setTimeout,放入对应的node.js的阶段中(timers),往下执行,发现nextTick,放入微任务中;再往下执行,还有同步代码,输出2。第一遍循环:输出1->2。
第二遍执行:取出第一遍执行的微任务nextTick,输出nextTick2,往下执行,发现有setTimeout,将其放入对应的阶段中(timers)。代码再往下执行,发现有nextTick,放入微任务中。这时node.js的 timers阶段会有两个setTimeout,它会先把阶段中的代码执行完,再执行微任务。因此,接下来输出的是第一遍执行时的setTimeout1,发现这个阶段还未执行完,紧接着输出setTimeout2.最后阶段转换,发现微任务,输出nextTick1。此时输出的顺序是:nextTick2->setTimeout1->setTimeout2->nextTick1
node.js中还有一些比较有趣的地方,这里就不讲述为什么了。只是简单列举出来
- process.nextTick()会优先于Promise.then()执行
- setImmdieate()和setTimeout()顺序不确定
小板凳坐等,欢迎各位大佬来撩!

参考资料