引言:通过第一关,我们知道JavaScript源代码在经过JS引擎的解析、解释等一系列操作后,源代码被转化成了字节码,那么这些字节码最终是如何被执行的呢?
目的:那么接下来让我们一起去了解字节码的整个执行过程吧。
1. 导读
在讲JavaScript源代码如何执行之前,我们先来了解一下解释性语言和编译型语言的概念。
- 解释型语言:程序不需要提前编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次。
- 编译型语言:程序提前使用编译器一次性将所有源代码编译为一个可执行程序,一次编译可重复执行。
JavaScript是一种解释性编程语言,因此其是一边解释一边执行的。
2.执行前逻辑
这里的执行机制,以Google公司的V8引擎(2017版)为例。流程如下:
- 首先,编写好的JavaScript源代码被加载到V8引擎中;
- 然后,V8引擎的解析器(Parser)将代码解析为抽象语法树(Abstract Syntax Tree,简称AST)和执行上下文;
- 最后,V8引擎的解释器(Ignition)将AST解释为字节码。
2.1 Parser再探究
Parser解析的时候并不会进行全量解析(全部解析1.耗时间;2.解析后的字节码需放入内存耗内存),而是有延迟解析的策略,也就是一种按需解析的方案。首先Parpaser会解析出所需的最少限度的内容,比如内部有未调用的函数,则解析出函数声明,只有当函数被调用时才对该函数进行完整的解析。
2.2 Ignition再探究
Ignition关注的是减少 V8 的内存开销,会进行执行前的优化工作。它会将AST进行分析将多次调用的函数标记为热点函数并交由TurboFan进行编译生成优化后的机器码执行,而单次调用的函数则会被生成字节码再做执行。
3. 执行时逻辑
当AST解释完成的时候,V8引擎的高性能解释器就会对字节码进行解释执行操作,流程如下:
- 首先,将解析器生成的对应执行上下文压入到执行上下文栈中;
- 然后,加载字节码,并将其转化成成机器码;
- 最后,进行代码的执行。
下面以一个简单的例子,详细讲述一下执行阶段流程中执行上下文的出入栈流程。
function a() {
console.log('a');
b();
}
function b() {
console.log('b');
}
a()
- 在执行代码之前,会将全局代码解析时所生成的Global Execute Context压入到栈中,并将全局代码放入call stack中执行;
- 执行a()函数之前,此时会对a()函数进行解析(延迟解析策略),生成a Execute Context,并将其压入到栈中,同时将a函数代码放入call stack中执行;
- 执行b()函数,此时会对b()函数进行解析(延迟解析策略),生成b Execute Context,并将其压入到栈中,同时将b函数代码放入call stack中执行;
- b()函数执行完毕,b Execute Contex从调用栈退出;
- a()函数执行完毕,a Execute Contex从调用栈退出;
- 全部代码执行完毕,Global Execute Contex从调用栈退出,结束执行;
4. 异步任务执行逻辑
我们知道JavaScript是单线程的,即同一时间调用栈中只能有一段代码被执行。那么按理论分析的话,当程序中有异步任务时,是不是就会出现需要等待异步任务结束,从而导致阻塞的情况呢?
那么实际上真如上述分析一样吗? 下面以一个小例子实际验证一下:
const a = 1;
setTimeout(()=> {
console.log('timeout');
}, 1000)
console.log(a)
按照理论上的分析,此时的输出结果应该依次是: timeout 1,并且timeout需要在1s之后才会输出; 但实际上的输出结果是: 1 timeout,并且整个程序没有出现阻塞的现象。
4.1 callback queue
那么V8引擎,是如何避免异步任务的阻塞呢? 不知道大家还记不记得在JS引擎和运行时这一关中,我们讲到了运行时,其中包含一个callback queue。当程序中的异步任执行后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行栈中的其他任务,当一个异步任务返回结果后,V8引擎会将这个事件加入到这个callback queue。当栈中任务执行完成之后,才会查询该队列中的回调函数,并将其放入到栈中执行,从而避免异步任务的阻塞。