深入理解 JavaScript 事件循环:从进程线程到 async/await

63 阅读4分钟

你是否曾经对 JavaScript 的执行机制感到困惑?为什么代码不是按顺序执行?为什么 setTimeout 的延迟时间不准确?今天我们就来彻底搞懂 JavaScript 的事件循环机制。

📚 基础概念

进程与线程

  • 进程:CPU 运行指令到加载和保存上下文环境所需要的时间开销
  • 线程:CPU 执行指令所需的时间开销

举个生活中的例子:当你打开 Chrome 浏览器,操作系统会为它创建一个进程。每新建一个标签页,就相当于增加一个进程。在每个标签页进程中,又包含多个线程:

  1. 渲染线程:负责页面的渲染和重绘
  2. JS 引擎线程:负责执行 JavaScript 代码
  3. HTTP 请求线程:处理网络请求

重要提示:因为 JavaScript 可以操作 DOM,为了线程安全,JS 引擎线程和渲染线程是互斥的。这就是为什么长时间运行的 JavaScript 代码会导致页面卡顿。

v8

  • v8在执行js的过程中默认只开一个线程

  • 只开一个线程就会带来异步问题

  • 单线程处理代码的过程:遇到同步任务就会立即执行,遇到异步任务就会存放到任务队列中,等待js引擎线程空闲时,在执行任务队列中的异步任务。

来看下面这段代码

let a=1
setTimeout(()=>{
    a=2
},1000)
console.log(a);

v8遇到耗时任务就会挂起,放在这样的一个任务队列中,并且这里是有顺序可言的。很显然这时候的setTimeout就被挂起所以这时候输出的a就是1.

39B8F0AE-AD7F-4E5C-ACC0-03EEF4E0390F.png 这时候来看一段进阶代码:

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里面的代码,但是里面还有一个定时器所以又直接放在宏任务中去,再去执行微任务里面的代码按顺序执行。

2A5A55C5-F0C9-4638-805B-FBD3BAA00E9A.png

代码执行顺序

  1. 先执行同步代码(这属于宏任务),这个过程如果遇到异步任务,就存入对应的队列
  2. 同步执行完之后,执行微任务队列中的代码
  3. 微任务全部执行完后,有需要的就渲染页面
  4. 执行宏任务(下一次循环的开始)

async/await 本质

  1. 函数前面加一个async等同于函数内部返回一个promise实例对象
  2. await 必须跟async 配合使用,并且await后面如果不接一个promise对象,await无法约束它
  3. 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 是单线程语言,通过事件循环实现异步,微任务优先于宏任务执行”