6

什么是事件循环?

JavaScript是一门单线程语言,即当有一个任务在执行的时候,其他任务需要在后面等待。但在实际场景中,有些任务可以放在比较靠后的位置,比如加载网络资源时发送的异步请求。

事件循环就是JavaScript异步执行机制的一种实现方式

JavaScript执行机制

前面说到执行上下文存在创建阶段和执行阶段,这一章节主要针对执行阶段来讲。即在分析JavaScript代码执行时,首先要做一次执行上下文创建阶段的分析,而后才能利用这篇文章的知识点来分析JavaScript的执行机制

我们常说JavaScript是按照代码顺序一行行执行下来的,但这段代码实际输出的顺序是2,1。这是为什么呢?

setTimeout(function(){
 console.log(1);
},0)
console.log(2);

JavaScript引擎会智能地将任务分成同步任务和异步任务。

  • 同步任务:主代码
  • 异步任务:setTimeout、Promise、setInterval、nextTick等等

首先JavaScript引擎会将整体script代码放入执行栈中,遇到同步任务则直接进入主线程执行,遇到异步任务时注册回调函数,并将异步任务放进事件队列中等待。

待主线程中的所有同步任务执行完毕后,才会去事件队列处取任务。根据队列先进先出的特点,最先进入事件队列的异步任务会被率先执行。

事件执行机制.jpg

而在实际场景中,并不是从事件队列一一取出异步任务直接执行这么简单。从宏观定义上,我们将JavaScript的任务分为同步任务和异步任务,但在事件循环中,更加精细的分法是将任务分为宏任务和微任务。不同类型的任务会进入不同的事件队列中。

  • 宏任务:一般的JavaScript同步代码,定时器相关的异步代码setTimeout、setInterval
  • 微任务:Promise、nextTick等

JavaScript引擎执行代码过程

在JavaScript引擎执行代码的过程中,首先将整体代码作为一个宏任务执行,遇到一般的同步代码立即执行,遇到定时器相关如setTimeout这种宏任务代码则先将其放入到宏任务事件队列中,遇到Promise这种则将其catch、then函数放入到微任务队列中。

整体代码作为第一次宏任务执行完毕后,JavaScript引擎会先到微任务队列中看看有没有任务,如有任务则将其全部执行,如果没有则进入到下一次宏任务中。也就是说JavaScript执行机制是按照宏任务-微任务-宏任务-微任务...这样子循环执行的。

根据队列先进先出的特点,取出宏任务事件队列中第一个宏任务率先执行,遇到同步代码立即执行,遇到微任务则继续进入微任务事件队列。待这一次的宏任务执行完毕后,则继续执行所有的微任务。如此循环下去,则就是事件循环。

宏任务与微任务.jpg

先举个简单的例子

 setTimeout(function(){
  console.log(0);
 },3000);
 new Promise((resolve,reject)=>{
   console.log(2);
   resolve();
 }).then(()=>{
     console.log('resolve');
  }
 );
 console.log(1)

第一遍事件循环过程:

  • 依据上述的结论,整体代码作为宏任务首先进入主线程中执行,setTimeout是一个宏任务,则先将其放入到宏任务事件队列中。
  • 继续下面的代码,遇到Promise里面的立即执行语句,则直接输出2。遇到then函数将其放入到微任务事件队列中,遇到最后一句输出1。
  • 在这一遍宏任务执行过程中,宏任务事件队列中有一个setTimeout在等待执行,微任务事件队列中有一个then函数在等待执行。
  • 则依据宏任务-微任务-宏任务-微任务的执行特点,接下来要去微任务事件队列中取任务,则跟着要输出then函数中的resolve。微任务事件队列仅有这一个微任务,至此微任务全部执行完毕。第一遍事件循环过程结束。

第二遍事件循环过程:

  • 第二遍事件循环仍旧从宏任务开始,要去宏任务事件队列中取一个宏任务执行,此时第一个宏任务是setTimeout,则直接执行输出0即可。
  • 再去微任务事件队列中查看是否还有微任务等待执行,发现微任务事件队列中没有任务。则跳过,继续去宏任务事件队列中查看是否有宏任务,依旧没有任务。至此事件循环结束。这段代码输出的顺序是2,1,resolve,0。

上面给定时器的延迟时间是3秒钟,有无可能是时间片竞争的机制抢着输出?试着把setTimeout的延迟时间改为0。

setTimeout(function(){
 console.log(0);
},0);
new Promise((resolve,reject)=>{
  console.log(2);
  resolve();
}).then(()=>{
    console.log('resolve');
 }
);
console.log(1)

发现输出的顺序一致,实际上将延迟设置为0,也不是立即执行。

依据上面的结论,将setTimeout的延迟时间设为0的意思是待主线程空闲后便可以执行,还得等主线程按照宏任务-微任务-宏任务-微任务的事件循环顺序执行完毕后。实际上即使主线程空闲了,延迟执行也不会达到0秒立即执行,最低也是4ms。

小结

  1. 同步任务进入主线程率先执行,异步任务注册回调函数后进入事件队列
  2. 宏任务:大部分JavaScript同步代码,定时器相关setTimeoutsetInterval
    微任务:PromisenextTick
  3. JavaScript执行机制按照宏任务-微任务-宏任务-微任务的循环顺序执行。注意在每轮事件循环过程中,执行宏任务是从宏任务事件队列中取出最早进入的一个任务,而微任务则是全部执行完毕。
参考文章:https://juejin.im/post/59e85e...

wuquan133
26 声望1 粉丝

正在成长中的小前端