前言
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的过程中只有一个线程会工作
- 节约性能
- 节约上下文切换的时间
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
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
- 第3行代码是
Promise
构造函数内部的同步操作,打印出2
resolve
已经被调用,第一个.then回调then1
被推入微任务队列等待执行。第二个.then回调then2
也被推入微任务队列,排在第一个.then之后。- 后面setTimeout设置为0毫秒延迟,但它仍然是宏任务,所以其回调被推入宏任务队列。
- 继续执行剩余的同步代码,第15行代码执行,打印出
6
。 - 执行完当前同步代码块后,事件循环会检查微任务队列 。
then1
被执行,打印出3
。 - 接着
then2
被执行,打印出4
。 - 当所有微任务执行完毕后,事件循环会检查宏任务队列 。此时,
setTimeout
的回调函数被执行,打印出5
。 - 输出结果为
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
- 第3行代码是
Promise
构造函数内部的同步操作,打印出2
resolve
已经被调用,第6行的.then回调被推入微任务队列等待执行。- 接着第12行的setTimeout的回调函数
set2
被推入宏任务队列。 - 继续执行剩余的同步代码,第18行代码执行,打印出
7
。 - 执行完当前同步代码块后,事件循环会检查微任务队列 。
then
被执行,第7行打印出3
。 - 在.then回调函数中发现宏任务setTimeout的回调函数
set1
,将其推入宏任务队列。 - 当所有微任务执行完毕后,事件循环会检查宏任务队列 。此时,宏任务队列中有
set2
和set1
,先执行第12行的setTimeout
的回调函数set2
,执行13行的同步代码,打印出5
,第二次事件循环开始。 - 在
set2
中发现宏任务set3
,将其推入宏任务队列。 - 接着去找微任务队列,发现没有微任务队列,那么就会去找宏任务队列。
- 此时的宏任务队列里面有
set1
和set3
。先执行第9行的set1
,第9行打印出4
,第三次事件循环开始。 - 然后又会去找微任务队列,发现没有微任务队列,那么就会去找宏任务队列。
- 此时的宏任务队列里面只有
set3
。执行第14行的set3
,第15行打印出6
。 - 输出结果为
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、2一样, 执行同步代码 ,第1行代码执行,打印结果
script start
。 - 代码执行到第9行时,调用 async1(),遇见 await async2(),开始执行 async2()。执行第7行,打印结果
async2 end
。 - 回到 async1(),await 后的代码也就是第4行被放入微任务队列等待执行。
- 第10行的setTimeout被推入宏任务队列。
- 第14行代码是
Promise
构造函数内部的同步操作,打印出promise
。 resolve
已经被调用,第17行的.then回调then1
被推入微任务队列等待执行。- 接着第20行的.then回调
then2
也被推入微任务队列等待执行。 - 继续执行剩余的同步代码,第23行代码执行,打印出
script end
。 - 执行完当前同步代码块后,事件循环会检查微任务队列 。先执行第4行,打印出
async1 end
;接着执行第18行,打印出then1
;执行微任务中的最后一个,打印出then2
。 - 当所有微任务执行完毕后,事件循环会检查宏任务队列 。此时,宏任务队列中有
setTimeout
,执行11行的同步代码,打印出setTimeout
。 - 输出结果如下图所示
结语
通过一系列实例,我们揭开了宏任务与微任务的神秘面纱,理解了它们在事件循环舞台上的角色分工与执行顺序。希望这篇文章可以给你带来帮助。