事件循环机制——解决异步代码导致的头脑风暴

100 阅读5分钟

前言

今天让我们来介绍js中的另一座大山——事件循环机制,我们现在都很熟悉能识别js语言的一个浏览器node,我们今天俩聊一聊js在浏览器下的机制是怎么样子的。

进程和线程

在我们聊今天的主角事件循环机制之前我们先来介绍一下进程和线程这个概念来便于我们深层次了解事件循环机制。

我们现在已经知道了js语言是一个单线程语言,在计算机上的所有操作都是由cpu执行的,cpu将要执行的操作分为一个个任务,这些任务就可理解为一个个进程,而这些任务又分为一些更小的子任务,这些子任务就可称为线程。所以进程为CPU 在运行指令和保存上下文所需要的时间,而线程为执行一段指令所需要的时间

比如:浏览器每开一个tab页,就是新开一个进程,而新开的这个进程又有渲染线程,js 引擎线程,http 线程等。进程和线程都大多都可以并发执行,但是渲染线程 和 js 引擎线程是互斥的,不能并发执行,因为如果两个线程同时操作DOM,可能会导致数据不一致和页面显示错误。更为抽象去理解可以理解为线程是进程的子集

js一定是单线程的吗?

我们从开始介绍js这门语言到现在一直提及js是一门单线程的语言,但从未说过在js中用韵只有一个线程被启动,其实不然,我们在学习js中只是默认只有一个线程被开启,但在人为操作的情况下是可以实现开启,新的线程,所以我们只能说js默认只有一个线程被开启。

event-loop

在我们介绍完线程和进程这个概念后,现在就让我们来深层次的开始聊一聊event-loop——事件循环机制。

我们在学习js语言时,代码被划分为了同步代码和异步代码,而在我们介绍promise的使用时讲了耗时代码定时器,但代码是否耗时不能成为区分同步代码和异步代码的标准。我们只能说耗时代码一定是异步代码。

对于同步代码我们没有什么过多去介绍的,相比之下读者大大们应该更想知道js中有哪些异步代码,特别是还有不耗时也会导致异步的代码那就让我们来好好聊一聊。

官方对异步代码还进行了划分

异步代码

  1. 微任务

  • promise.then()(异步操作出现在promise身上的then)
  • process.nextTick()
  • MutationObserver.observe()
  1. 宏任务

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI-rendering

我们在v8疫情的执行下异步代码会被分为微任务和宏任务,在其执行过程这两类代码在v8执行过程中我们之前讲到会被挂起,等执行完同步代码再去执行异步代码,它们其实是分别会放在微任务队列和宏任务队列中的,执行异步代码的过程中呢我们得先执行微任务队列中的代码,然后再执行宏任务队列。

我们根据上面描述来对代码执行顺序做总结如下:

eventloop步骤:

  1. 执行同步代码(这属于宏任务)
  2. 执行完同步后检查是否有异步代码需要执行
  3. 执行所有的微任务
  4. 如果有需要就渲染页面
  5. 执行宏任务,也是开启了下一次事件循环 在第一次事件循环当中宏任务先行,之后可以说微任务比宏任务先行 带你过一次事件的执行的开始也是上一次时间宏任务的结束

我们面提到了微任务队列和宏任务队列,那我们来根据实际代码分析来看一下他们是如何存放的。代码如下:

console.log(1);//1
new Promise(function(resolve, reject) {
  console.log(2);//2
  resolve()
})
.then(() => {
  console.log(3);//4
  setTimeout(() => {
    console.log(4);//6
  }, 0)
})
setTimeout(() => {
  console.log(5);//5
  setTimeout(() => {
    console.log(6);//7
  }, 0)
}, 0)
console.log(7);//3

我们来分析这段代码输出顺序:首先我们要先进行同步代码先输出1,再往下看,我们看到了Promise但我们上面说过promise后的then才是异步代码中的微任务,那么直接输出2,再之后我们看到了then,那就把它存入微任务队列中,那么then里面的内容等到从微任务队列中拿出后才去执行,往下看到定时器把它放到宏任务对列中,最后输出7,以上我们的同步代码执行结束,现在开始执行异步代码,根据规则我们先执行微任务,再去执行宏任务,最后得到输出结果。

我们根据图来分析微任务和宏任务是如何存入微任务队列和宏任务队列中的。

image.png 我们来看:当我们先把.then放入微任务队列中,后把定时器放在宏任务对类中,当我们先去执行微任务代码时发现.then中有一个定时器,那就要把这个定时器再放入第一个之后以此类推得到上面的图。

注意:

  1. 浏览器对 await 的执行提前了。 (await 后面的代码当成同步来执行)
  2. 会将后续(await 所在代码的下一行代码)全部代码挤入微任务队列

拓展

我们在面试的过程中可能会被问到setTimeout 定时器执行的时间准吗?

settTimeout被执行时浏览器会启动一个新的线程来计时,等到时间结束才将定时器的回调取出来执行,(js 主线程将其取出) 当js主线程还在继续执行同步代码,而定时器在浏览器中定时时间已经结束,那么定时器中的回调会被挂起,直到同步执行完毕,微任务也执行完毕,才执行该回调。