JavaScript的事件循环(Event Loop)

我们知道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的代码执行方式如下:

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却是每隔这一个时间间隔都执行一次,直到收到取消指令,如果一个定时任务的执行时间比时间间隔本身长,会累积执行时间的误差,而且还会造成任务堆积。

JavaScript 复制代码
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)

处理方式

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

JavaScript 复制代码
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()

forEach中的异步

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

JavaScript 复制代码
const urls = ['https://juejin.cn','https://www.zhihu.com', 'https://baidu.com'];
urls.forEach(async (url) => {
  const response = await fetch(url);
  console.log(response);
});

forEach无法使用async/await的原因

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

解决方案

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

JavaScript 复制代码
const urls = ['https://juejin.cn','https://www.zhihu.com', 'https://baidu.com'];
for (const url of urls) {
  const response = await fetch(url);
  console.log(response);
}

JavaScript中的"多线程"

HTML5中提出了Web Worker,它允许在 JavaScript 中创建多线程环境,从此出现JavaScript不再是单线程的说法开始出现。其实这个说法是错的,因为JavaScript一直以来都是单线程的脚本语言,Web Worker只是创建了一个线程环境,并运行在这个线程中运行JavaScript,并且该线程不会影响到主线程,因而看起来像是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)
相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角6 小时前
CSS 颜色
前端·css
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
光头程序员8 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript