JavaScript与Event Loop

347 阅读6分钟

看标题,我想大家应该猜到我要说什么了,在我在谈论这个话题之前首先需要先说明一点:接下来的描述可能会存在错误,和许多我自己的观点。如有不对的地方还请大家帮忙指正。

说到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的数量达到系统的最大数量时,进入下一阶段。这六个阶段都执行完毕称为一轮循环。

在node.js中也存在微任务(microtask),process.nextTick()可以看成是一个微任务。只是它执行的顺序与浏览器的不同。node.js中,微任务是在阶段转化时,才会执行

我们再来看上面那段代码的执行顺序。

第一遍执行:代码自上而下执行,遇到同步代码,先输出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()顺序不确定

小板凳坐等,欢迎各位大佬来撩!

参考资料

cnode.js 不要混淆nodejs和浏览器中的event loop

event loop的规范实现