likes
comments
collection
share

JavaScript的事件循环(Event Loop)

作者站长头像
站长
· 阅读数 21

我们知道JavaScript是一种同步的、阻塞的、单线程的语言,这篇文章的内容就是介绍JavaScript事件循环。

同步任务和异步任务

好比一个人要吃饭,肯定是要经过洗手然后吃饭的步骤,他自己没有办法在洗手的同时还能吃饭。但是在做饭的时候一般都是煮饭和炒菜同时进行,而不是一定要盯着电饭锅直到把饭煮好才能炒菜。这种一边煮饭一遍炒菜的方式我们称之为异步

同步任务:指在主线程上执行的任务,其特点是只有当前一个任务执行完成才能执行下一个任务。

异步任务:由JS委托给宿主环境执行,等执行完成之后再通知主线程执行异步任务的回调函数。

线程与进程

进程:进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位,操作系统的其他所有内容都是围绕着进程展开的,负责执行这些任务的是cup。

线程:线程是操作系统能够进行运算调度的最小单位,其是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。

举个例子:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)

浏览器多线程

浏览器是JavaScript的宿主之一,他有着很多进程,其中的**渲染进程(Renderer Process)**就是我们平时用于执行JavaScript的进程。渲染进程中又有好几个线程,这些线程相互协调,我们的js代码便良好地运行起来了。

GUI渲染线程

  • GUI线程负责渲染页面,解析HTML和CSS,构建成DOM树和渲染树。
  • 界面重绘和回流也会执行
  • 该线程JS引擎线程是互斥的

JS引擎线程(JavaScript Engine Thread)

  • JS的引擎,负责处理Javasc的代码(如V8引擎)。
  • JavaScript说的单线程指的就是该线程
  • JS引擎线程执行是GUI渲染线程会被挂起(他们是互斥的)

主线程(Main Thread)

  • 浏览器的线程,存在一个事件队列,用于控制事件循环。
  • 当js引擎执行异步任务时会将这些任务加入事件队列等js引擎空闲时候处理

定时触发器线程(Timer Thread)

  • 执行setIntervalsetTimeout的线程
  • 定时器由浏览器的定时触发器线程计时,计时完毕后添加到事件队列中(放入事件触发线程中)
  • W3C在HTML标准中规定要求setTimeout中低于4ms的时间间隔为4ms(也就是0ms也算4ms)

异步HTTP请求线程

  • 在XMLHttpRequest连接后是通过浏览器新开的一个线程请求
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中(放入事件触发线程中)。再由JavaScript引擎执行

Web Worker 线程:

  • 用于执行长时间运行的JavaScript代码,但是与主线程和 GUI 渲染线程是完全独立的,不会阻塞主线程的执行

什么是事件循环Event Loop

事件循环(Event Loop)是一种用于处理异步任务和事件的机制,常见于浏览器环境和Node.js环境中。它是使得 JavaScript 能够处理非阻塞异步操作的核心机制,使代码能够按照一定的顺序执行,同时处理异步任务,而不会阻塞主线程的执行。

我们知道,JavaScript是一种同步的、阻塞的、单线程的语言,那这个意思就是js的代码只能是一行一行执行下来的,不过有意思的是由于setTimeoutpromise 等异步任务,我们可以先执行一部分代码,然后再执行异步任务里的代码。JS的代码执行方式如下:

JavaScript的事件循环(Event Loop)

setTimeoutpromise 等回调函数虽然都是异步任务,同样的代码无论setTimeout 是在promise 前面还是后面,setTimeout 返回的结果总是会比promise 慢,这是怎么一回事呢?要分析这个问题,我们就需要引入宏任务和微任务这两个概念了。

宏任务(macroTask)和微任务(microTask)

我们把宿主环境提供的任务称为宏任务,把由 JavaScript 引擎自身提供的任务称为微任务。并且微任务优先级比宏任务更高。

宏任务主要有:

  1. setTimeout 和 setInterval:用于在一定时间后执行回调函数或定期执行回调函数
  2. XMLHttpRequest 和 Fetch:用于进行网络请求,获取数据
  3. DOM事件:例如点击事件、输入事件等。
  4. requestAnimationFrame:用于在下一次重绘之前执行回调函数,通常用于动画效果。
  5. 页面渲染:当浏览器需要重新渲染页面时触发的任务。
  6. 文件读写:例如使用 FileReader 进行文件读取。
  7. 文件 I/O(Nodejs):例如读写文件。
  8. 网络 I/O(Nodejs):例如进行网络请求,与外部服务器进行通信。

微任务主要有:

  1. Promise.then(非 new Promise):当 Promise 状态变为 resolved 或 rejected 时,会产生微任务。
  2. MutationObserver:用于监听 DOM 树的变化,一旦发现 DOM 变化,会产生微任务。:用于监听 DOM 树的变化,一旦发现 DOM 变化,会产生微任务。
  3. process.nextTick(Nodejs): 用于在当前执行栈结束后立即执行回调函数。

关于setTimeout 和 setInterval

  1. setTimeout: 用于在一段时间后执行一次指定的函数或代码块。
  2. setInterval: 用于以固定的时间间隔重复执行指定的函数或代码块。

setInterval的隐患

二者的作用都是在间隔某段时间后去执行指定的函数,但是不同的是setTimeout只执行一次,而setInterval却是每隔这一个时间间隔都执行一次,直到收到取消指令,如果一个定时任务的执行时间比时间间隔本身长,会累积执行时间的误差,而且还会造成任务堆积。

let startTime = +new Date()
setInterval(() => {
  console.log('间隔时间:'+ (+new Date() - startTime))
  const count = Math.floor(10000000000  * Math.random())
  for (let i = 0;i < count;i++) { }
  startTime = +new Date()
}, 1000)

JavaScript的事件循环(Event Loop)

处理方式

基于setInterval存在的隐患,我们可以试着用setTimeout去实现setInterval,这样可以避免这种误差,并且保证执行完任务才重新开启宏任务,不会造成任务堆积。

let startTime = +new Date()
function doTask() {
   setTimeout(() => {
    console.log('间隔时间:'+ (+new Date() - startTime))
    const count = Math.floor(10000000000  * Math.random())
    for (let i = 0;i < count;i++) { }
    startTime = +new Date()
    doTask()
  }, 1000)
}
doTask()

JavaScript的事件循环(Event Loop)

forEach中的异步

我们有时候可能会接到一些需求,要循环调用几个接口,并且他们是有先后顺序的,我们会想到用forEach去解决这个问题,但是当我们使用的时候就会发现,循环的结果与我们想要的不一致。

JavaScript的事件循环(Event Loop)

forEach无法使用async/await的原因

forEach方法会在当前作用域内同步执行回调函数,不会创建一个新的作用域,因此不会等待异步操作完成,而是继续迭代,尽管使用了async/await,但它们是无效的。

解决方案

for...of 循环和 for...in 循环以及 for 循环都可以使用 async/await,因此我们可以使用它们替代forEach

JavaScript的事件循环(Event Loop)

JavaScript中的“多线程”

HTML5中提出了Web Worker,它允许在 JavaScript 中创建多线程环境,从此出现JavaScript不再是单线程的说法开始出现。其实这个说法是错的,因为JavaScript一直以来都是单线程的脚本语言,Web Worker只是创建了一个线程环境,并运行在这个线程中运行JavaScript,并且该线程不会影响到主线程,因而看起来像是JavaScript变成了多线程而已。

最后

最后我们举一道经典的异步题目,如果能自己算出这道题,那么说明你对事件循环的理解已经可以了。如果算的结果和代码运行的结果不一致,那这里就是你的短处,需要再理解一遍我们的知识点。

console.log(1);

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

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
  
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

setTimeout(() => {
   console.log(9)
}, 0);
console.log(10)