十分钟带你深入了解JS事件循环机制

前言

在当今的Web开发中,JavaScript扮演着核心角色。它不仅驱动着网页的交互,更是实现复杂应用的关键。然而,JavaScript的单线程特性带来了挑战:如何在不阻塞用户界面的情况下处理大量异步操作?这就引出了事件循环机制------JavaScript的心脏,它通过协调任务的执行,确保了程序的流畅运行。本文将深入探讨这一机制,揭示其背后的原理和工作方式。


正文

同步代码

同步代码 等待任务完成才能继续执行下一行代码。也就是说在同步操作完成之前,程序会暂停执行,不会进行任何其他操作。通常适用于那些执行时间非常短的任务,比如简单的数学运算本地变量赋值等。

示例代码

javascript 复制代码
console.log('同步代码开始');
let sum = 5 + 3; // 同步操作,立即得到结果
console.log('5 + 3 的结果是', sum);
console.log('同步代码结束');

异步代码

异步代码允许程序在等待某些操作完成时继续执行其他任务。

异步代码是需要耗时的代码,其中的任务队列又分为微任务宏任务

微任务

微任务(Microtasks)是JavaScript中用于快速响应异步事件的轻量级任务,它们在当前宏任务完成后立即执行,优先于下一个宏任务。主要包括:

  1. Promise.then() : 用于注册Promise解决或拒绝时的回调函数,这些回调作为微任务加入队列。
  2. process.nextTick() : Node.js特有的方法,允许在当前操作完成后立即执行回调,作为微任务。
  3. MutationObserver: 监听DOM变化的Web API,当检测到变化时,其回调函数作为微任务执行。

宏任务

宏任务(Macrotasks)是JavaScript事件循环中的大任务单元,它们通常表示一些较大的操作或延时执行的任务,主要包括:

  1. script: 浏览器加载和解析JavaScript脚本文件,整个脚本作为一个宏任务执行。
  2. setTimeout: 设置一个定时器,当指定时间到达后,其回调函数作为一个宏任务被加入队列。
  3. setInterval : 与setTimeout类似,但会周期性地重复执行回调函数,每次执行都是一个宏任务。
  4. setImmediate : 在Node.js中,用于在当前事件循环结束后立即执行回调函数。在浏览器中,可以通过setTimeout(callback, 0)实现相似效果。
  5. I/U: 包括文件读写、网络请求等,完成后的回调函数作为宏任务处理。
  6. UI-rendering:浏览器自动更新页面元素的视觉表示,以反映最新的DOM和样式变更的过程。

示例代码

javascript 复制代码
console.log('异步代码开始');
setTimeout(() => {
  console.log('这是异步操作,不会阻塞主线程');
}, 1000);
console.log('异步代码结束');

尽管setTimeout函数设置了1秒的延迟,但程序不会等待这1秒结束,而是会立即执行下一行代码,即打印"异步代码结束",然后继续执行其他任务。1秒后,setTimeout中的回调函数会被执行:


进程和线程

  • 进程:CPU运行指令和保存上下文所需的时间
  • 线程:执行一段指令需要的时间

比如:一个浏览器的tab页面

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

js的加载是会阻塞页面渲染的,即渲染线程和js引擎线程是不能同时工作的

多线程

  • 定义:任务分散到多个线程上并行执行。
  • 优点:能更好地利用多核处理器,提高计算效率。
  • 缺点:增加了编程复杂性,需要管理线程间的同步。
  • 应用:适合CPU密集型任务,如视频编码、3D渲染等

单线程

  • 定义:所有任务在同一个线程上按顺序执行。
  • 优点:简单,避免了并发问题,易于开发和调试。
  • 缺点:长时间运行的任务可能阻塞后续任务执行。
  • 应用:适合I/O密集型任务,如JavaScript在浏览器中的执行。

js的单线程

v8在执行js的过程中,只有一个线程会工作

  1. 节约性能
  2. 节约上下文切换的时间

Event-Loop步骤

  1. 执行同步代码(这属于是宏任务)
  2. 同步执行完毕以后,检查是否有异步需要执行
  3. 执行所有的微任务
  4. 微任务执行完毕以后,如果有需要就会渲染页面
  5. 执行异步宏任务,也是开启下一次事件循环

我们来看下面的示例

javascript 复制代码
console.log(1);
new Promise((resolve, reject) => {
 console.log(2);
 resolve()
})
 .then(() => {
   console.log(3);
   setTimeout(() => {
     console.log(4);
   }, 0)
 })
setTimeout(() => {
 console.log(5);
 setTimeout(() => {
   console.log(6);
 }, 0)
}, 0)
console.log(7);

分析

(为了方便理解,笔者这里会用一些"代号"代替某段代码)

我们按照Event-Loop的步骤进行逐步解释:

  1. console.log(1)是同步任务,直接输出 1
  2. new Promise()是一个函数的调用,并不是微任务里的Promise.then(),所以它其实是一个同步的代码。执行输出 2
  3. .then()3这是一个微任务,要进微任务队列(这里用then代替),之后直接看setTimeOut()5
  1. setTimeOut()5,看到是一个定时器,直接放入宏任务队列(这里用set1代替);
  1. console.log(7)输出7,到这里Event-Loop的第一步就结束了;

  2. 检查是否有异步代码要执行,先看微任务队列,有.then(console.log(3)),先直接输出3

  3. .then里的setTimeout()4,是宏任务,放入宏任务队列(这里用set2 代替),此时.then已经执行完了,出微任务队列,第一次的事件也已经执行完了,现在开始下一次宏任务;

  4. 宏任务队列中现在是有setTimeout()5setTimeout()4,本着队列先进先出的原则,先执行set1 ,里面有 console.log(5)直接输出5

  5. 发现setTimeout()5里还有一个setTimeout()6,这个是第二次事件循环发现的,啥也别说了,你也进宏队列(set3代替);

  1. 我们此时的微任务队列都已经执行完了,就一个.then()嘛,可以直接看宏任务队列
  2. set2 ,对应的是setTimeOut()4这段代码,里面的console.log(4)直接执行输出4set2执行完毕,出队。第二次事件循环结束,开启下一个宏任务。
  3. 没有微任务,执行set3 ,是setTimeOut()6这段,执行console.log(6)输出 6,结束! 看运行结果:

async

async 是 JavaScript 中的一个关键字,用于声明一个异步函数

  1. 声明方式 :使用 async 关键字声明的函数会在其内部自动返回一个 Promise 对象。
  2. 返回值 :如果函数正常执行完毕,Promise 会被解决(resolved)并返回函数的返回值;如果函数中抛出错误,Promise 会被拒绝(rejected)。
  3. 错误处理 :使用 try...catch 语句在异步函数内部捕获错误,防止 Promise 被隐式拒绝。
  4. await 关键字 :在 async 函数内部,可以使用 await 关键字等待一个 Promise 解决。await 只能在 async 函数内部使用。

示例

javascript 复制代码
// 声明一个异步函数
async function fetchData() {
  try {
    // 使用 await 等待一个 Promise 解决
    const data = await fetch('https://api.example.com/data');
    const result = await data.json();
    console.log(result);
  } catch (error) {
    console.error('An error occurred:', error);
  }
}

// 调用异步函数
fetchData();

在这个示例中,fetchData 是一个异步函数,它使用 await 等待 fetch 请求完成并解析 JSON 数据。如果在等待过程中发生错误,错误会被 catch 块捕获。

使用 asyncawait 可以简化异步代码的编写,使代码看起来更像是同步的,同时保持了异步执行的优势。

代码理解

javascript 复制代码
console.log('script start');
async function async1() {
  await async2()
  console.log('async1 end');
}
async function async2() {
  console.log('async2 end');
}
async1()
setTimeout(function () {
  console.log('setTimeout');
}, 0)
new Promise(function (resolve, reject) {
  console.log('promise');
  resolve()
})
  .then(() => {
    console.log('then1');
  })
  .then(() => {
    console.log('then2');
  })
console.log('script end');

第一遍:

第二遍:

运行结果:

结语

以上就是本篇文章的全部内容,JS的事件循环机制是有点绕,但是根据示例代码来进行逐步分析就能很好地理解,希望本篇文章对读者有所帮助,感谢阅读!

相关推荐
运维@小兵31 分钟前
vue使用路由技术实现登录成功后跳转到首页
前端·javascript·vue.js
肠胃炎33 分钟前
React构建组件
前端·javascript·react.js
邝邝邝邝丹39 分钟前
React学习———React.memo、useMemo和useCallback
javascript·学习·react.js
美酒没故事°1 小时前
纯css实现蜂窝效果
前端·javascript·css
GISer_Jing2 小时前
React useState 的同步/异步行为及设计原理解析
前端·javascript·react.js
mini榴莲炸弹2 小时前
什么是SparkONYarn模式?
前端·javascript·ajax
能来帮帮蒟蒻吗2 小时前
VUE3 -综合实践(Mock+Axios+ElementPlus)
前端·javascript·vue.js·笔记·学习·ajax·typescript
啊啊啊~~2 小时前
歌词滚动效果
javascript·html
球球和皮皮3 小时前
Babylon.js学习之路《四、Babylon.js 中的相机(Camera)与视角控制》
javascript·3d·前端框架·babylon.js
njsgcs4 小时前
opencascade.js stp vite webpack 调试笔记
开发语言·前端·javascript