通过3个实例深入理解v8中的事件循环机制

前言

V8是Google开发的开源高性能JavaScript引擎,被用于Chrome浏览器和Node.js等环境中。V8与事件循环的运作密切相关,它负责执行由事件循环机制调度的JavaScript代码。

正文

同步与异步代码

  • 同步代码(不耗时):同步代码指的是程序按照书写顺序依次执行的代码。如果这段同步代码执行迅速,不涉及长时间的等待(如I/O操作、网络请求等),我们通常称之为"不耗时"的同步代码。

  • 异步代码(耗时):异步代码则允许程序在等待某个操作(如文件读写、网络请求、数据库查询等可能需要较长时间的操作)完成的同时,继续执行后续的其他任务,而不是阻塞在那里。

js 复制代码
let a = 2;
console.log(a);// 打印结果为2

setTimeout (function() { // 代码耗时,先挂起
    a++;
},1000)

console.log(a);// 打印结果为2

在上面的代码中,会先执行同步代码,再执行异步代码。定时器是一个耗时的代码,所以会先挂起,先去执行第8行的代码,a打印出来的结果都是2。

进程和线程

  • 进程:进程是一个正在执行的程序实例,是系统进行资源分配和调度的基本单位。每个进程都有独立的内存空间、系统资源和至少一个执行线程。
  • 线程:线程是进程内的一个执行单元,是CPU调度的基本单位。一个进程可以包含一个或多个线程,这些线程共享该进程的内存空间和资源。

JS是单线程的

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

  1. 节约性能
  2. 节约上下文切换的时间
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.27/vue.cjs.js"></script>
</head>
<body>
  <script>
    // vue
  </script>
  
  <p>hello world</p>
</body>
</html>

js的加载是会阻塞页面的渲染的,渲染线程和js引擎线程是不能同时工作的。当浏览器遇到<script>标签时,它会暂停当前的HTML解析,等待脚本下载(如果脚本是外部的)并执行完毕。在此期间,浏览器不会继续解析后续的HTML内容,也不会进行渲染工作。

浏览器中的事件循环(Event Loop)

事件循环主要分为两个阶段:宏任务(Macrotasks)和微任务(Microtasks)。

  • 宏任务:包括script代码的执行、setTimeout、setInterval、setImmediate、I/O、UI渲染。
  • 微任务:包括Promise.then()、MutationObserver()、process.nextTick()。

执行流程分为如下5个步骤(重点)

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

实例

实例1

js 复制代码
console.log(1);// 第1个执行
new Promise((resolve, reject) => {
    console.log(2);// 第2个执行
    resolve();
})
.then(() => {
    console.log(3);// 进微任务队列 第4个执行
})
.then(() => {
    console.log(4);// 进微任务队列 第5个执行
})
setTimeout(() => {
    console.log(5);// 进宏任务队列 第6个执行
})
console.log(6);// 第3个执行

在上面的代码中,执行的步骤如下所示:

  1. 执行同步代码 ,第1行代码执行,打印结果1
  2. 第3行代码是Promise构造函数内部的同步操作,打印出2
  3. resolve已经被调用,第一个.then回调then1被推入微任务队列等待执行。第二个.then回调then2也被推入微任务队列,排在第一个.then之后。
  4. 后面setTimeout设置为0毫秒延迟,但它仍然是宏任务,所以其回调被推入宏任务队列。
  5. 继续执行剩余的同步代码,第15行代码执行,打印出6
  6. 执行完当前同步代码块后,事件循环会检查微任务队列then1被执行,打印出3
  7. 接着 then2被执行,打印出4
  8. 当所有微任务执行完毕后,事件循环会检查宏任务队列 。此时,setTimeout的回调函数被执行,打印出5
  9. 输出结果为 1 2 6 3 4 5

实例2

js 复制代码
console.log(1);// 第1个执行
new Promise((resolve, reject) => {
  console.log(2);// 第2个执行
  resolve()
})
.then(() => {
  console.log(3);// 进微任务队列 第4个执行
  setTimeout(() => {
    console.log(4);// set1第二个进宏任务队列 第6个执行
  }, 0)
})
setTimeout(() => {
  console.log(5);// set2第一个进宏任务队列 第5个执行
  setTimeout(() => {
    console.log(6);// set3第三个进宏任务队列 第7个执行
  }, 0)
}, 0)
console.log(7);// 第3个执行

在上面的代码中,执行的步骤如下所示:

  1. 前几步和实例1是一样的, 执行同步代码 ,第1行代码执行,打印结果1
  2. 第3行代码是Promise构造函数内部的同步操作,打印出2
  3. resolve已经被调用,第6行的.then回调被推入微任务队列等待执行。
  4. 接着第12行的setTimeout的回调函数set2被推入宏任务队列。
  5. 继续执行剩余的同步代码,第18行代码执行,打印出7
  6. 执行完当前同步代码块后,事件循环会检查微任务队列then被执行,第7行打印出3
  7. 在.then回调函数中发现宏任务setTimeout的回调函数set1,将其推入宏任务队列。
  8. 当所有微任务执行完毕后,事件循环会检查宏任务队列 。此时,宏任务队列中有set2set1,先执行第12行的setTimeout的回调函数set2,执行13行的同步代码,打印出5,第二次事件循环开始。
  9. set2中发现宏任务set3,将其推入宏任务队列。
  10. 接着去找微任务队列,发现没有微任务队列,那么就会去找宏任务队列。
  11. 此时的宏任务队列里面有set1set3。先执行第9行的set1,第9行打印出4,第三次事件循环开始。
  12. 然后又会去找微任务队列,发现没有微任务队列,那么就会去找宏任务队列。
  13. 此时的宏任务队列里面只有set3。执行第14行的set3,第15行打印出6
  14. 输出结果为 1 2 7 3 5 4 6

实例3

js 复制代码
console.log('script start');// 第1个执行
async function async1() {
  await async2();// 等async2执行  await会将后续的代码阻塞进微任务队列
  console.log('async1 end');// 第一个进入微任务队列,第5个执行
}
async function async2() {
  console.log('async2 end');// 第2个执行
}
async1();
setTimeout(function() {
  console.log('setTimeout');// 进入宏任务队列,第8个执行
}, 0)
new Promise(function(resolve, reject) {
  console.log('promise');// 第3个执行
  resolve();
})
.then(() => {
  console.log('then1');// 第二个进入微任务队列,第6个执行
})
.then(() => {
  console.log('then2');// 第三个进入微任务队列,第7个执行
})
console.log('script end');// 第4个执行

在上面的代码中,执行的步骤如下所示:

  1. 前几步和实例1、2一样, 执行同步代码 ,第1行代码执行,打印结果script start
  2. 代码执行到第9行时,调用 async1(),遇见 await async2(),开始执行 async2()。执行第7行,打印结果async2 end
  3. 回到 async1(),await 后的代码也就是第4行被放入微任务队列等待执行。
  4. 第10行的setTimeout被推入宏任务队列。
  5. 第14行代码是Promise构造函数内部的同步操作,打印出promise
  6. resolve已经被调用,第17行的.then回调then1被推入微任务队列等待执行。
  7. 接着第20行的.then回调then2也被推入微任务队列等待执行。
  8. 继续执行剩余的同步代码,第23行代码执行,打印出script end
  9. 执行完当前同步代码块后,事件循环会检查微任务队列 。先执行第4行,打印出async1 end;接着执行第18行,打印出then1;执行微任务中的最后一个,打印出then2
  10. 当所有微任务执行完毕后,事件循环会检查宏任务队列 。此时,宏任务队列中有setTimeout,执行11行的同步代码,打印出setTimeout
  11. 输出结果如下图所示

结语

通过一系列实例,我们揭开了宏任务与微任务的神秘面纱,理解了它们在事件循环舞台上的角色分工与执行顺序。希望这篇文章可以给你带来帮助。

相关推荐
什么鬼昵称17 分钟前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色35 分钟前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2341 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河1 小时前
CSS总结
前端·css
NiNg_1_2341 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦1 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
BigYe程普1 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠1 小时前
如何通过js加载css和html
javascript·css·html
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发