"转动的旋律:在V8中探索事件循环机制的魅力"

375 阅读5分钟

事件循环机制

前言: v8

V8 是 Google 开发的一款高性能的 JavaScript 和 WebAssembly 引擎,广泛用于 Chrome 浏览器和 Node.js 运行时环境。V8 的事件循环(Event Loop)是其处理异步操作的核心机制之一

概念

事件循环机制是计算机编程中一种常见的模式,尤其是在处理输入输出操作以及需要响应用户交互的程序设计中。它主要应用于异步编程模型中,可以有效地管理多个并发任务而不阻碍程序的运行。事件循环机制的核心思想是在一个循环中等待并响应来自不同来源的事件或消息。

let a=1  
console.log(a);  
setTimeout(function (){  
a++  
},1000)  
console.log(a)//1

这段代码的执行结果为 1 1 并且等待了一秒后才执行完毕。由此可见定时器这个函数是被v8引擎挂起了

异步与同步

同步代码我们可以认为是不需要耗时就能执行的,同理异步代码就是需要耗时才会执行的。 我们假设这种情况函数a是耗时的,函数b是不耗时的。函数b的调用依赖函数a的执行结果。那么会出现什么情况呢?v8会把耗时的代码先挂起,然后执行不耗时的代码。

进程和线程

那什么叫做进程呢?手机或电脑的cpu在运行一个指令以及在加载上下文所需要的时间。我们可以举一个通俗的例子:比如我们用手机打开微信。等微信完全加载完毕那这就叫做一个进程。或者我们在浏览器上开了多个窗口,我们也可以称这是一个个进程。 线程又是什么呢?线程本质是执行一段指令所需要的时间,它是进程的更小部分。 我们可以试想一下当我们打开了一个浏览器的tab页面它是由许许多多的线程构成的。其中包括渲染线程,js引擎线程,http线程......

js引擎线程与渲染线程

前面我们聊到了页面中js引擎线程和渲染线程。那么它们之间会有怎样的影响呢?

<!DOCTYPE html>  
<html lang="en">  
<head>  
<meta charset="UTF-8">  
<title>Title</title>  
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.33/vue.cjs.js"></script>  
</head>  
<body>  
   <p>hello world</p>  
</body>  
</html>

代码是从上往下执行的,当浏览器执行到第6行的时候,浏览器势必会朝着这个地址把在线的js读取到并进行加载,这是一个耗时的过程。 由此可见js的加载是会阻塞页面的渲染的。js引擎线程和渲染线程是不能同时工作的。我们试想一下假如它们能同时工作的话,就会出现这样的情况js引擎线程在执行1s的时候刚好执行到某一行的js代码。而js这门脚本语言能操作DOM结构,这一行代码的使用会改变DOM结构。我们又刚好假设它们能同时工作1s后刚好渲染线程进行。这时候就会遇到这有的问题究竟是渲染更改后的DOM结构还是渲染原来DOM结构呢?这就会造成一个不安全的渲染页面就不知道渲染。

js的单线程

v8在执行的过程中,只有一个线程会工作,正是因为只有一个线程工作,所以代码被分为同步代码和异步代码,当遇到需要耗时的代码的时候v8就会将它挂起。腾出手来执行同步代码。换个角度想如果没有这个机制的话js这门语言的效率将会极其低下。 其实单线程的好处也很明显——可以节约性能。也可以节约上下文切换的时间。

异步代码中的微任务和宏任务

微任务

有如下:promise.then(),process.nextTick(), MutationOvserver()

宏任务

script,seTimeout,setInterval,setImmediate, setTimeout 函数允许你在指定的时间后只运行一次某个函数。 setInterval 函数允许你每隔一段固定的时间就执行一次某个函数。 setImmediate 是一个 Node.js 中提供的 API,该方法用来把一些需要长时间运行的操作放在一个回调函数里边,在浏览器完成后面的其它语句后就立即执行这个回调函数。 I/O(输入,输出),UI-rendering(页面渲染)。

event-loop循环原理

正因为js是单线程语言,且有同步和异步代码。不能同步代码和异步代码一起跑,所以才会有事件循环机制,js引擎线程会在宏任务和微任务到处跑。 1.执行同步代码(这属于宏任务) 2.同步代码执行完毕后,检查是否有异步需要执行 3.执行所有微任务 4.微任务执行完毕后,如果有需要就会渲染页面 5.执行异步宏任务,也是开启下一次事件循环。 接下来我们上代码来试试~~

console.log(1);  
new Promise((resolve,reject)=>{  
console.log(2);  
resolve()  
}).then(()=>{  
console.log(3)  
}).then(()=>{  
console.log(4);  
})  
setTimeout(()=>{  
console.log(5);  
})  
console.log(6)

执行结果为1 2 6 3 4 5。 代码先执行宏任务 输出1 2 6。然后执行微任务3,4,5 宏任务会被放进宏任务队列。队列呢是先进先出的。微任务也会被放入在微任务队列。

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)

输出结果为:// 1,2,7,3,4,5,6

接下来我们来看看终极代码~~


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')

我们先看看async await的功能!! async 申明一个函数就相当于套了一层return new Promise(),使得函数的返回值为一个Promise对象。wait 关键字用于等待一个 Promise 对象完成(即 resolved 或 rejected)。await 必须在一个由 async 关键字定义的函数内部使用。

值得注意的是await 会将后续的代码放入微任务队列!!由此我们可以得到执行结果: script start async2 end promise script end async1 end then1 then2 setTimeout