教你成为JS的“时间管理大师”——事件循环机制

183 阅读11分钟

前言

在之前的文章(解决异步的大好人——Promise)中我们提到js是一门单线程的语言,在它执行的过程中碰到需要耗时的异步代码会直接将其挂起,然后先执行不耗时的同步代码。我们只是浅显的了解了这位“时间管理大师”的表层效果,下面我们来学习一下如何成为真正的“时间管理大师”。

1733367321956.png

1. 进程和线程

现在的时间呢,也马上大学生期末考试和考研了,我们先来给大家简单复习一下进程和线程这两个东西的关系。可能有些同学老是会搞混这两个东西的关系,简单来说呢它们之间的关系就类似于父与子,进程是爸爸线程是好大儿下面一张图来表示一下它们的关系:

image.png

dabbc5b1d6e0d928d8cd000517d2719.jpg

通过上面这张图我们可以得知线程是进程的一个子集,在了解完关系之后,我们来看看这两个东西分别是什么:

  1. 进程:CPU 在运行指令和保存上下文所需要的时间(进程之间相互不影响)
  2. 线程:执行一段指令所需要的时间

这么聊它们两个的概念可能有点抽象,下面我们来通过js来聊聊这两个东西。在js中我们在node中运行代码的时候就相当于是开了一个进程,而由于js是单线程的所以js在执行过程中会开一个线程在这个大的进程中。除了在js中,我们在日常使用浏览器时也会开进程和线程,这两个东西可以把它们当成是两个单位。

比如:浏览器每开一个 tab 页面就是新开一个进程

在浏览器中有下面几个线程

  1. 渲染线程
  2. js 引擎线程
  3. http 线程

在我们打开一个页面时,页面渲染和js的执行是息息相关的,在这个过程中我们得注意渲染线程 和 js 引擎线程不能同时工作。这个原因大家可以先理解为:由于js可以操作html,当我们两个同时工作的话,由于js可以获取dom元素,当js执行到某一段需要获取元素,但是页面还没有渲染出来那js上哪找去是吧。

在了解完了进程和线程的概念以及页面渲染和js线程之后,下面问大家一个问题:js 代码的加载会不会阻塞页面的渲染? 答案是会,js的加载是js引擎线程在工作

2. js是单线程?

现在来个小插曲纠正一下大家之前的观念,在我们初学js的时候,都在说js是一门单线程语言,这个思想观念已经根深蒂固在大家内心深处了,下面呢我就得让大家道心破碎一下,正所谓不破不立嘛,js也可以不是单线程的。

1733367565399.png

在 v8 引擎运行js代码时,这个进程中默认只有一个线程会被开启,但是我们在新版本中可以人为开启另一个线程,而这时js就不是单线程的了,这里大家记住如果以后面试官问你的时候别头铁说是了奥。

3. eventloop——“时间管理大师”

在前面对线程和进程有了一个基础的概念之后,下面我们也是跨过了初学者的门槛有了成为“时间管理大师”的资格了,下面我们来看看js中的“时间管理大师”——事件循环机制

eventloop:v8 按照先执行同步,再执行异步的策略,反复重复

在之前的文章异步中我们说到它还有个死对头叫同步代码,之前我们说到需要消耗时间的就是异步代码,不需要消耗时间的是同步代码,在这里我得问问大家了不耗时的代码一定是同步代码吗?大家都知道for循环吧,它循环几千次不需要时间,那如果十万次呢是不是得等一会,那这个会耗时吗?这个就和我们电脑自带的cpu有关了如果是超级计算机可能很快,但是如果是我们自己的电脑就会很慢才计算出结果。这个for其实是同步代码大家别被误导了,所以说呢这个耗时的代码说是我们平常的耗时其实是不准确的,而是v8中的耗时的异步代码。下面我们来看一段代码:

console.log(1);
Promise.resolve().then(() => {
  console.log(2);
})
console.log(3);

// 输出:
// 1
// 3
// 2

大家可以在自己的电脑上试一下上面代码运行的结果是什么,结果出来后大家可能会觉得奇怪,这些代码明明没有计时器那种要消耗时间都是同步代码呀,为什么会被挂起然后最后执行呢?这就不得不提到异步代码和同步代码有哪些了,首先我们得记住下面一点:

耗时的代码一定不是同步代码,不耗时的代码不一定是同步代码

同步代码和异步代码

在了解完了事件循环机制的基本概念,下面得先跟大家聊一下同步代码和异步代码有哪些才能给大家成为时间管理大师打好基础:

  1. 同步代码

  • 除了异步代码中微任务和宏任务中的代码其他都是同步
  1. 异步代码

  • 微任务

  1. promise身上的.then(),

  2. process.nextTick()(浏览器环境没有node环境才有),

  3. MutationObserver()

  • 宏任务

  1. html中的script标签
  2. setTimeout
  3. setInterval
  4. setImmediate
  5. I/O
  6. UI-rendering

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

eventloop步骤

在我们了解完了微任务和宏任务有哪些后,下面我们就该聊聊如何成为大师了:

eventloop步骤

  1. 执行同步代码(这属于宏任务)
  2. 执行完同步后,检查是否有异步代码(将两个任务分别放入任务队列中)需要执行
  3. 执行所有的微任务
  4. 如果有需要,就渲染页面
  5. 执行宏任务,也是开启了下一次事件循环(第一辆火车的火车尾是下一辆火车的火车头)

步骤2中说到了将两个任务分别放入任务队列中,而任务队列呢就跟数据结构中的那个队列一样先进先出,如果有两个都是微任务那么哪个先进入队列,哪个就先执行,宏任务也一样,下面用一段代码来展示一下:

console.log(1);
Promise.resolve().then(() => {
  console.log(2);
})
Promise.resolve().then(() => {
  console.log(4);
})
console.log(3);

// 输出:
// 1
// 3
// 2
// 4

我们可以看到异步执行的输出结果是先输出2,然后再输出4,这样可能有些抽象下面我们来画图给大家展示一下: 首先下面这张图是js从上到下先执行同步代码的过程中微任务队列中的内容变化(同步代码的执行就不展示了,就是从上往下按顺序执行):

image.png 当我们执行完了同步代码之后来执行异步中的微任务队列中的代码,遵循先进先出的规则:

image.png 以上的过程就是js中有微任务的时候的执行过程,下面我们再来看看既有微任务,又有宏任务的时候是如何执行的,下面来看一段代码:

console.log(1);
Promise.resolve()
.then(() => {
  console.log(2);
})
.then(() => {
  console.log(3);
})

setTimeout(() => {
  console.log(4);
}, 0)
console.log(5);

// 输出:
// 1 5 2 3 4

上面代码既有宏任务也有微任务,并且微任务还是两个连接在一起的那么执行过程中是如何的呢,下面我们来看看微任务队列和宏任务队列中的任务变换情况(同步代码过程省略,第一行是执行完了同步之后异步队列中的情况):

image.png 根据上面代码我们发现eventloop这个“时间管理大师”当遇到有两个.then()嵌套的时候,会先执行完前面那个,然后再执行后面那个有个先来后到的顺序,这个方法同样适用于宏任务队列。下面我们来看一个小例子看看大家有没有成为“时间管理大师“的入场券:

console.log(1);
new Promise((resolve, reject) => {
  console.log(2);//这个new Promise也是同步代码
  resolve()
})
.then(() => {
  console.log(3);
  setTimeout(() => {
    console.log(4);
  }, 0)
})
setTimeout(() => {
  console.log(5);
  setTimeout(() => {
    console.log(6);
  }, 0)
}, 0)
console.log(7);

// 输出:
// 1 2 7 3 5 4 6

大家可以自己先试试看能不能根据前面所讲的步骤自己想象出来任务队列中的执行过程,有点不懂也没关系,下面我来为大家展示一下:

image.png

在看了上面的步骤我们就会问了,微任务一定比宏任务先走吗?

不一定,在第一次时间循环当中,宏任务(执行同步代码属于宏任务)先行,之后可以说微任务比宏任务先行,不能绝对说微任务比宏任务先行。

eventloop中的 await && async

大家学到这里已经是一名半”时间管理大师“了,所谓大师要的就是精益求精,我们都知道es6新增了一个东西叫await还有async,这个东西用Promise实现的话可以用.then。这里有同学可能会猜测这个用.then那它应该是异步了。这个await后面接的代码其实是同步代码这里必须得注意了:

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

下面呢我们来一个机缘帮助大家成为真正的大师:

a9732362b81440e367b5db16d0626f4.jpg

console.log('script start');
async function async1() {
  await async2() 
  console.log('async1 end');
}
async function async2() {
  console.log('async2 end');
}

async1()
setTimeout(() => {
  console.log('setTimeout');
}, 0)
 
new Promise((resolve, reject) => {
  console.log('promise');
  resolve()
})
.then(() => {
  console.log('then1');
})
.then(() => {
  console.log('then2');
});
console.log('script end');

下面给大家看一下执行顺序:

script start
async2 end
promise
script end
async1 end
then1
then2
setTimeout

上面的async中的代码得注意await async2()中会让async2()当成同步代码执行,然后将await下面的console.log('async1 end');先放入微任务队列中,下面给大家画一下两个任务队列中的变化情况:

image.png

4. 面试官:setTimeout 定时器执行的时间准吗?

在经历了前面的九九八十一难之后,咱们终于来到了最后一关经过这一关咱就能成为”时间管理大师了“,我们在使用setTimeout这个定时器的时候有没有想过,它的时间一定准吗?答案是否定的,这个问题呢也常常出现于面试之中下面我们为大家讲解一下原因:

这个原因呢用咱们的大白话来说,setTimeout做完了它的工作之后,就对js说:

c2d30471c8333e4122f863fda577aff.jpg 这个时候js由于手头还有事情没有干完,就想让他先等等就会对setTimeout说:

68f59f0cfea8668a3eac819a5966b8d.jpg

于是呢js在干完了手头的同步代码和微任务后才会去执行这个setTimeout,下面呢用标准一点的口吻跟大家说明一下:

根本原因:setTimeout被执行时,浏览器会启动一个新的线程(不是v8中,js在v8中默认是单线程)来计时,等到时间结束才将定时器的回调取出来执行(js主线程负责将其取出)。如果此时js主线程还在执行同步代码,js主线程会让计时器其中的回调一直挂起,直到主线程同步代码执行完毕,微任务也执行完毕,才执行该回调。

结语

以上呢是教我们如何成为一名真正的js”时间管理大师“的教程,希望为大家成为大师的路上提供一点帮助,祝大家早日成为大师。

1732338928918.jpg