简明扼要的JavaScript运行机制:从V8引擎到事件循环的协作

104 阅读5分钟

全文没有特殊提及的引擎外均为V8引擎

V8是运行时环境,这个相信大家一定清楚。

为了获得速度,V8JavaScript代码转换为更高效的机器代码,而不是使用解释器。它通过使用JIT(Just-In-Time)编译器(注意:JIT编译器是一部分编译器的类别,并非一个编译器的名字),在执行时将JavaScript代码编译为机器代码。V8不生成字节码或任何中间代码。

V8引擎在内部是多个线程的:

  • 主线程会执行一些操作:获取代码、编译代码并执行代码
  • 编译线程专注于优化代码。在主进程执行代码的时候,编译线程会分析主线程执行代码的路径(通常是“热点”代码,即被频繁调用的代码段),并将这些“热点”代码优化为更高效的机器码。一旦优化结束,编译线程会将优化后的代码交给主线程使用,替换未来要运行的代码路径
  • 垃圾回收线程用于管理内存,回收未使用的对象并且防止内存泄漏,提高内存使用效率
  • 分析器(profiler)线程可以告诉我们在哪些方法上花费了太久的时间,以便优化器可以优化他们(提供开发的性能分析服务)
  • 并发线程、I/O线程、WebAssembly线程等等

V8由两方面组成:

  • 内存堆:这是内存分配发生的地方
  • 调用栈:这是代码执行的地方

每一个进入调用栈的都称为调用帧,因为JavaScript是单线程的,运行代码很容易,不必处理多线程环境中出现的复杂场景,例如死锁,但是在一个线程上运行也会非常受限制。

实际上是可以同步实现Ajax请求的,如下代码示例:

function fetchDataSync(url) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url, false); 
  xhr.send();
​
  if (xhr.status === 200) {
    return JSON.parse(xhr.responseText);
  } else {
    throw new Error(`HTTP error! status: ${xhr.status}`);
  }
}
try {
  const data = fetchDataSync('https://xxx.com');
  console.log('Fetched data:', data);
} catch (error) {
  console.error('Error:', error);
}

尽管JavaScript允许异步的代码(就像是我们刚刚说的setTimeout) ,但直到ES6JavaScript自身从未有过任何关于异步的直接概念。

能够运行JavaScript的环境(浏览器、Nodejs等等)都拥有一个事件循环的内置机制, JS引擎只是任意的JS代码按需执行的环境。是它周围的环境来调度这些事件(JS代码执行)。

事件循环的核心职责为:监听事件、调度任务和协调执行

因此,JavaScript引擎只负责执行代码,何时执行、执行什么代码是由运行环境的事件循环机制决定的

举个形象一点的例子:

  • JavaScript引擎是演员,只负责按照剧本表演(执行代码)
  • 宿主环境提供的事件循环是导演,负责调度演员,安排出场顺序

比如:

  1. 一个定时器setTimeout被设置,宿主环境记录定时器,并在到达时间后,将其回调函数加入任务队列。
  2. 事件循环监控任务队列,一旦主线程空闲,就从队列中取出回调,交给JavaScript引擎执行。

image-20241210205906874.png 我这里有一段代码:

setTimeout(fun(), 1000)

这个代码替换掉上图代码块中的代码,执行结果仍然是一致的,但是要强调这并不意味着fun()会在1s后执行,而是意味着在1s过后fun()会在事件循环的控制下,放入事件队列中。放入事件队列中就意味着会被立即执行?当然不是!这个队列中可能会错开时间优先执行更早被添加到事件队列的事件,你的fun()会按照顺序等待执行。

在ES6中新引入了一种概念叫做作业队列(Job Queue),这个通常在现今被称为微任务队列(与之相对的是宏任务队列)。微任务队列是JS引擎管理的一种高优先级异步任务队列,用于存放Promise的回调或者一些特定的异步任务(async/await的后续代码,queueMicotask等)。

常见的宏任务:

  • setTimeout
  • setInterval
  • setImmediate(Node.js专有)
  • I/O操作
  • UI渲染任务(浏览器专有)

常见的微任务就宽泛的理解为异步任务吧!

通常一个宏任务会伴随着一个微任务队列,这说明通常宏任务是和微任务交替进行工作的,两者分别在引擎和宿主环境中通过事件循环机制紧密协作。

微任务队列的优先级是高于宏任务队列的,所以他们的执行时机就是主进程执行完同步任务后,立即清空微任务队列中的内容,将其放入调用栈执行,一个宏任务执行完成后,再清空微任务...直至任务队列中没有任务

image-20241210211634301.png

微任务队列不需要经过Web APIs,而是直接进入微任务队列,由引擎在当前事件循环结束时清空。这与宏任务不同,宏任务的许多来源通常是由浏览器或Node.js的API提供支持。

上面提到了,JS引擎实际上不具有事件循环的机制,这就意味着通常是宿主环境对任务队列进行管理细分(例如宏任务队列和微任务队列)。

Chrome浏览器中,宏任务队列由ChromeBlink引擎管理,而微任务由V8引擎管理。

JavaScript的运行机制就像一场精心编排的舞台剧——V8引擎是演员,事件循环是导演,宏任务和微任务则是按照剧本安排出场的角色。每一次代码执行,都是一场充满默契的协作表演。所以,下次当你写setTimeout或Promise时,不妨想象自己正在排演一场舞台剧。谁会先登场,谁又会压轴出场,关键就看你的“导演”如何调度!想要成为这场大戏的主导者,深入理解这些机制,就是你手握话筒的第一步:)