全文没有特殊提及的引擎外均为
V8
引擎
V8
是运行时环境,这个相信大家一定清楚。
为了获得速度,V8
将JavaScript
代码转换为更高效的机器代码,而不是使用解释器。它通过使用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
) ,但直到ES6
,JavaScript
自身从未有过任何关于异步的直接概念。
能够运行JavaScript
的环境(浏览器、Nodejs等等)都拥有一个事件循环
的内置机制, JS引擎只是任意的JS代码按需执行的环境。是它周围的环境来调度这些事件(JS代码执行)。
事件循环的核心职责为:监听事件、调度任务和协调执行
因此,JavaScript引擎
只负责执行代码,何时执行、执行什么代码是由运行环境的事件循环机制决定的。
举个形象一点的例子:
JavaScript引擎
是演员,只负责按照剧本表演(执行代码)- 宿主环境提供的事件循环是导演,负责调度演员,安排出场顺序
比如:
- 一个定时器
setTimeout
被设置,宿主环境记录定时器,并在到达时间后,将其回调函数加入任务队列。 - 事件循环监控任务队列,一旦主线程空闲,就从队列中取出回调,交给
JavaScript引擎
执行。
我这里有一段代码:
setTimeout(fun(), 1000)
这个代码替换掉上图代码块中的代码,执行结果仍然是一致的,但是要强调这并不意味着fun()
会在1s
后执行,而是意味着在1s
过后fun()
会在事件循环
的控制下,放入事件队列
中。放入事件队列
中就意味着会被立即执行?当然不是!这个队列中可能会错开时间优先执行更早被添加到事件队列
的事件,你的fun()
会按照顺序等待执行。
在ES6中新引入了一种概念叫做作业队列(Job Queue)
,这个通常在现今被称为微任务队列
(与之相对的是宏任务队列
)。微任务队列
是JS引擎管理的一种高优先级异步任务队列
,用于存放Promise
的回调或者一些特定的异步任务(async/await的后续代码,queueMicotask等)。
常见的宏任务:
- setTimeout
- setInterval
- setImmediate(Node.js专有)
- I/O操作
- UI渲染任务(浏览器专有)
常见的微任务就宽泛的理解为异步任务吧!
通常一个宏任务会伴随着一个微任务队列,这说明通常宏任务是和微任务交替进行工作的,两者分别在引擎和宿主环境中通过事件循环机制紧密协作。
微任务队列
的优先级是高于宏任务队列
的,所以他们的执行时机就是主进程执行完同步任务后,立即清空微任务队列中的内容,将其放入调用栈执行,一个宏任务执行完成后,再清空微任务...直至任务队列中没有任务
微任务队列
不需要经过Web APIs
,而是直接进入微任务队列,由引擎在当前事件循环结束时清空。这与宏任务不同,宏任务的许多来源通常是由浏览器或Node.js的API
提供支持。
上面提到了,JS引擎实际上不具有事件循环
的机制,这就意味着通常是宿主环境
对任务队列进行管理细分(例如宏任务队列和微任务队列)。
在Chrome
浏览器中,宏任务队列由Chrome
的Blink
引擎管理,而微任务由V8
引擎管理。
JavaScript的运行机制就像一场精心编排的舞台剧——V8
引擎是演员,事件循环
是导演,宏任务和微任务则是按照剧本安排出场的角色。每一次代码执行,都是一场充满默契的协作表演。所以,下次当你写setTimeout或Promise时,不妨想象自己正在排演一场舞台剧。谁会先登场,谁又会压轴出场,关键就看你的“导演”如何调度!想要成为这场大戏的主导者,深入理解这些机制,就是你手握话筒的第一步:)