2024-09-04 深入JavaScript高级语法十五——浏览器原理-V8引擎-js执行原理
目录
- 1、浏览器的工作原理
- 1.1、认识浏览器内核
- 1.2、浏览器渲染过程
- 2、JS引擎
- 2.1、认识 JavaScript 引擎
- 2.2、浏览器内核和JS引擎的关系
- 2.3、V8引擎的原理
- 2.4、V8引擎的架构
- 2.5、V8执行的细节
- 3、全局代码的执行过程
- 3.1、初始化全局对象
- 3.2、执行上下文栈(调用栈)
- 3.3、全局代码里包含函数时的执行过程
- 3.4、函数调用函数的执行过程-引出作用域提升的概念
- 3.5、概念:变量环境和记录
- 3.6、作用域提升面试题
- 3.7、vscode中的画图插件
1、浏览器的工作原理
1.1、认识浏览器内核
- 我们经常会说:不同的浏览器由不同的内核组成
- Gecko :早期被 Netscape 和 Mozilla Firefox 浏览器浏览器使用;
- Trident :微软开发,被IE4~IE11浏览器使用,但是 Edge 浏览器已经转向 Blink ;
- Webkit :苹果基于 KHTML 开发、开源的,用于 Safari , Google Chrome 之前也在使用;
- Blink :是 Webkit 的一个分支, Google 开发,目前应用于 Google Chrome 、 Edge 、 Opera 等;
- 等等…
- 事实上,我们经常说的浏览器内核指的是浏览器的排版引擎:
- 排版引擎( layout engine ),也称为浏览器引擎( browser engine )、页面渲染引擎( rendering engine )或样版引擎。
1.2、浏览器渲染过程
- 在下图的执行过程中, HTML 解析的时候遇到了 JavaScript 标签,应该怎么办呢?
- 会停止解析 HTML ,而去加载和执行 JavaScript 代码;
- 那么, JavaScript 代码由谁来执行呢?
- JavaScript 引擎
2、JS引擎
2.1、认识 JavaScript 引擎
- 为什么需要 JavaScript 引擎呢?
- 我们前面说过,高级的编程语言都是需要转成最终的机器指令来执行的;
- 事实上我们编写的 JavaScript 无论你交给浏览器或者 Node 执行,最后都是需要被 CPU 执行的;
- 但是 CPU 只认识自己的指令集,实际上是机器语言,才能被 CPU 所执行;
- 所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行;
- 比较常见的 JavaScript 引擎有哪些呢?
- SpiderMonkey :第一款 JavaScript 引擎,由 Brendan Eich 开发(也就是 JavaScript 作者);
- Chakra :微软开发,用于 IE 浏览器;
- JavaScriptCore : WebKit 中的 JavaScript 引擎, Apple 公司开发;
- V8:Google开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出;
- 等等…
2.2、浏览器内核和JS引擎的关系
- 这里我们先以 WebKit 为例, WebKit 事实上由两部分组成的:
- WebCore :负责 HTML 解析、布局、渲染等等相关的工作;
- JavaScriptCore :解析、执行 JavaScript 代码;
- 看到这里,学过小程序的同学有没有感觉非常的熟悉呢?
- 在小程序中编写的 JavaScript 代码就是被 JSCore 执行的;
- 另外一个强大的 JavaScript 引擎就是V8引擎。
2.3、V8引擎的原理
- 我们来看一下官方对V8引擎的定义:
- V8是用 C++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chrome 和 Node.js 等。
- 它实现 ECMAScript 和 WebAssembly ,并在 Windows 7或更高版本, macOS 10.12+和使用x64, IA-32, ARM 或 MIPS 处理器的 Linux 系统上运行。
- V8可以独立运行,也可以嵌入到任何 C ++应用程序中。
2.4、V8引擎的架构
AST解析查看
- V8引擎本身的源码非常复杂,大概有超过100w行 C ++代码,通过了解它的架构,我们可以知道它是如何对 JavaScript 执行的:
- Parse模块会将 JavaScript 代码转换成 AST (抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;
- 如果函数没有被调用,那么是不会被转换成 AST 的;
- Parse的V8官方文档:Parse的V8官方文档
- Ignition 是一个解释器,会将 AST 转换成 ByteCode (字节码)
- 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次, Ignition 会解析执行 ByteCode ;
- Ignition的V8官方文档:Ignition的V8官方文档
- TurboFan 是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码;
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为 ByteCode ,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
- TurboFan的V8官方文档: TurboFan的V8官方文档
2.5、V8执行的细节
- 那么我们的 JavaScript 源码是如何被解析( Parse 过程)的呢?
- Blink 将源码交给V8引擎, Stream 获取到源码并且进行编码转换;
- Scanner 会进行词法分析( lexical analysis ),词法分析会将代码转换成 tokens ;
- 接下来 tokens 会被转换成 AST 树,经过 Parser 和 PreParser :
- Parser 就是直接将 tokens 转成 AST 树架构;
- PreParser 称之为预解析,为什么需要预解析呢?
√ 这是因为并不是所有的 JavaScript 代码,在一开始时就会被执行。那么对所有的 JavaScript 代码进行解析,必然会影响网页的运行效率;
√ 所以V8引擎就实现了 Lazy Parsing (延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;
√ 比如我们在一个函数 outer 内部定义了另外一个函数 inner ,那么 inner 函数就会进行预解析;
- 生成 AST 树后,会被 lgnition 转成字节码( bytecode ),之后的过程就是代码的执行过程(后续会详细分析)。
3、全局代码的执行过程
假如我们有下面一段代码,它在JavaScript中是如何被执行的呢?
var name = 'why'
function foo() {
var name = 'foo'
console.log(name);
}
var num1 = 20
var num2 = 30
var result = num1 + num2
console.log(result);
foo()
3.1、初始化全局对象
- js 引擎会在执行代码之前,会在堆内存中创建一个全局对象: Global Object ( GO )口该对象所有的作用域( scope )都可以访问;
- 该对象所有的作用域( scope )都可以访问;
- 里面会包含 Date 、 Array 、 String 、 Number 、 setTimeout 、 setInterval 等等;
- 其中还有一个 window 属性指向自己;
3.2、执行上下文栈(调用栈)
- js 引擎内部有一个执行上下文栈( Execution Context Stack ,简称 ECS ),它是用于执行代码的调用栈。
- 那么现在它要执行谁呢?执行的是全局的代码块:
- 全局的代码块为了执行会构建一个 Global Execution Context ( GEC );
- GEC 会被放入到 ECS 中执行;
- GEC 被放入到 ECS 中里面包含两部分内容:
- 第一部分:在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 GlobalObject 中,但是并不会赋值;
√ 这个过程也称之为变量的作用域提升( hoisting )
- 第二部分:在代码执行中,对变量赋值,或者执行其他的函数;
3.3、全局代码里包含函数时的执行过程
- 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文( Functional Execution Context ,简称 FEC ),并且压入到 EC Stack 中。
- FEC 中包含三部分内容:
- 第一部分:在解析函数成为 AST 树结构时,会创建一个 Activation Object ( AO ):
√ AO 中包含形参、 arguments 、函数定义和指向函数对象、定义的变量;- 第二部分:作用域链:由 VO (在函数中就是 AO 对象)和父级 VO 组成,查找时会一层层查找;
- 第三部分: this 绑定的值;
- 只有一层函数
var name = 'why'
foo(123)
function foo(num) {
console.log(m);
var m = 10
var n = 20
console.log('foo');
}
- 函数嵌套时
var name = 'why'
foo(123)
function foo(num) {
console.log(m);
var m = 10
var n = 20
function bar() {
console.log(name);
}
bar()
}
3.4、函数调用函数的执行过程-引出作用域提升的概念
foo的父级作用域是GO。函数的父级作用域跟它的调用位置无关,只跟它的定义位置有关。
var message = 'hello global'
function foo() {
console.log(message); // 'hello global'
}
function bar() {
var message = 'hello bar'
foo()
}
bar()
3.5、概念:变量环境和记录
- 其实我们上面的讲解都是基于早期 ECMA 的版本规范:
Every execution context has associated with it a variable object . Variables and functions declared in the source text are added as properties of the variable object . For function code , parameters are added as properties of the variable object .
每一个执行上下文会被关联到一个变量环境( variable object , VO ),在源代码中的变量和函数声明会被作为属性添加到 VO 中。
对于函数来说,参数也会被添加到 VO 中。
- 在最新的 ECMA 的版本规范中,对于一些词汇进行了修改:
Every execution context has an associated VariableEnvironment . Variables and functions declared in ECMAScript code evaluated in an execution context are added as bindings in that VariableEnvironment ’ s Environment Record . For function code , parameters are also added as bindings to that Environment Record .
每一个执行上下文会关联到一个变量环境( VariableEnvironment )中,在执行代码中变量和函数的声明会作为环境记录( Environment Record )添加到变量环境中。
对于函数来说,参数也会被作为环境记录添加到变量环境中。
- 通过上面的变化我们可以知道,在最新的 ECMA 标准中,我们前面的变量对象 VO 已经有另外一个称呼了变量环境 VE 。
3.6、作用域提升面试题
- 面试题一
var n = 100
function foo() {
n = 200
}
foo()
console.log(n);
- 面试题二
function foo() {
console.log(n);
var n = 200
console.log(n);
}
var n = 100
foo()
- 面试题三
var n = 100
function foo1() {
console.log(n);
}
function foo2() {
var n = 200
console.log(n);
foo1()
}
foo2()
console.log(n);
- 面试题四
var a = 100
function foo() {
console.log(a);
return
var a = 100
}
foo()
- 面试题五
代码会报错,m is not defined
function foo() {
var m = 100
}
foo()
console.log(m);
- 面试题六
注意:这种写法在严格意义上算是一种语法错误,只是在JS引擎中对这种语法做了特殊处理,可能会出现在面试题中,但实际开发千万不要写这种代码。
function foo() {
m = 100
}
foo()
console.log(m);
- 面试题七
function foo() {
var a = b = 10
// => 转成下面的两行代码
// var a = 10
// b = 10
}
foo()
console.log(a);
console.log(b);