你是否曾经对 JavaScript 的执行机制感到困惑?为什么代码不是按顺序执行?为什么 setTimeout 的延迟时间不准确?今天我们就来彻底搞懂 JavaScript 的事件循环机制。
📚 基础概念
进程与线程
- 进程:CPU 运行指令到加载和保存上下文环境所需要的时间开销
- 线程:CPU 执行指令所需的时间开销
举个生活中的例子:当你打开 Chrome 浏览器,操作系统会为它创建一个进程。每新建一个标签页,就相当于增加一个进程。在每个标签页进程中,又包含多个线程:
- 渲染线程:负责页面的渲染和重绘
- JS 引擎线程:负责执行 JavaScript 代码
- HTTP 请求线程:处理网络请求
重要提示:因为 JavaScript 可以操作 DOM,为了线程安全,JS 引擎线程和渲染线程是互斥的。这就是为什么长时间运行的 JavaScript 代码会导致页面卡顿。
v8
-
v8在执行js的过程中默认只开一个线程
-
只开一个线程就会带来异步问题
-
单线程处理代码的过程:遇到同步任务就会立即执行,遇到异步任务就会存放到任务队列中,等待js引擎线程空闲时,在执行任务队列中的异步任务。
来看下面这段代码
let a=1
setTimeout(()=>{
a=2
},1000)
console.log(a);
v8遇到耗时任务就会挂起,放在这样的一个任务队列中,并且这里是有顺序可言的。很显然这时候的setTimeout就被挂起所以这时候输出的a就是1.
这时候来看一段进阶代码:
console.log(1);//1
new Promise((resolve) => {
console.log(2);//2
resolve()
})
.then(() => {
console.log(3);//4
setTimeout(() => {
console.log(4);//6
}, 0)
})
setTimeout(() => {
console.log(5);//5
setTimeout(() => {
console.log(6);//7
}, 0)
}, 0)
console.log(7);//3
这里的定时器虽然时间为0,但是它还是属于是异步任务,这里promise也是立马触发,只有状态变成resolve,才执行.then里面的代码,这时候这段代码就是不耗时的,但是把resolve加上定时器,.then这时候就变成了耗时代码。基于这种原因v8升级了,它把异步任务再进行了划分。
Event Loop(事件循环)
-
微任务:promise.then() ,process.nextTick() ,MuationObserver
-
宏任务:script(整体代码),setTimeout() ,setInterval(), ajax,I/O操作,UI-reendering
这两者都叫异步,这时候挂起就需要两个队列,一个微任务队列,和一个宏任务队列。这时候代码执行完同步代码,执行异步代码到第六行.then时就把这里面的代码放在微任务队列中,然后就是把12行的整个定时器放在宏任务中去,等到同步任务结束就开始处理微任务,也就是then里面的代码,但是里面还有一个定时器所以又直接放在宏任务中去,再去执行微任务里面的代码按顺序执行。
代码执行顺序
- 先执行同步代码(这属于宏任务),这个过程如果遇到异步任务,就存入对应的队列
- 同步执行完之后,执行微任务队列中的代码
- 微任务全部执行完后,有需要的就渲染页面
- 执行宏任务(下一次循环的开始)
async/await 本质
- 函数前面加一个async等同于函数内部返回一个promise实例对象
- await 必须跟async 配合使用,并且await后面如果不接一个promise对象,await无法约束它
- await fn() 把 fn() 当成同步看待,因为await会把它后续的代码挤到微任务中去,宏任务就还在宏任务中。
// 1. async 函数返回 Promise
async function test() {
return 1;
}
// 等价于
function test() {
return Promise.resolve(1);
}
// 2. await 会把后面的代码变成微任务
async function demo() {
console.log('A');
await Promise.resolve();
console.log('B'); // 这个变成微任务
}
一句话总结:await 后面的代码会进入微任务队列等待
“JavaScript 是单线程语言,通过事件循环实现异步,微任务优先于宏任务执行”