浏览器的工作原理
大家有没有深入思考过:JavaScript代码,在浏览器中是如何被执行的?
浏览器的渲染过程
-
在这个执行过程中,HTML解析的时候遇到了JavaScript标签,应该怎么办呢?
会停止解析HTML,而去加载和执行JavaScript代码;
-
那么,JavaScript代码由谁来执行呢?
JavaScript引擎
-
为什么需要JavaScript引擎呢?
高级的编程语言都是需要转成最终的机器指令来执行的;
事实上我们编写的JavaScript无论你交给浏览器或者Node执行,最后都是需要被CPU执行的;
但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行;
所以我们需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行;
V8引擎
-
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 ++应用程序中
代码被解析,v8引擎内部会帮助我们创建一个对象(GlobalObject -> go)
-
该对象所有的作用域(scope)都可以访问;
-
里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
-
其中还有一个window属性指向自己;
运行代码
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈
-
V8引擎的架构
- V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:
- Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
- 如果函数没有被调用,那么是不会被转换成AST的;
- Parse的V8官方文档:v8.dev/blog/scanne…
- Ignition是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition会执行解释执行ByteCode;
- Ignition的V8官方文档:v8.dev/blog/igniti…
- TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
- TurboFan的V8官方文档:v8.dev/blog/turbof…
-
V8引擎的解析图(官方)
-
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树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行过程(后续会详细分析)。
-
JavaScript的执行过程
1. 初始化全局对象
- js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象 所有的作用域(scope)都可以访问;
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
- 其中还有一个window属性指向自己;
2. 执行上下文栈(调用栈)
- js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。
- 那么现在它要执行谁呢?执行的是全局的代码块:
- 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
- GEC会 被放入到ECS中 执行;
- GEC被放入到ECS中里面包含两部分内容:
- 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;
- 这个过程也称之为变量的作用域提升(hoisting)
- 第二部分:在代码执行中,对变量赋值,或者执行其他的函数;
- 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;
3.GEC被放入到ECS中
4. GEC开始执行代码
作用域提升
第二行打印num1 会显示undefined
全局代码执行过程
-
函数
这里打印name,在foo函数内部找不到name,根据变量的真实查找路径是沿着作用域链来查找的规则,就会在父级作用域里找name,这里foo的父级作用域就是全局,所以打印出来就是why
-
函数嵌套
在foo函数中再嵌套一个bar函数,在bar函数中打印这个name会怎么样呢?
- 在代码执行前(预编译),在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值,bar函数这里保存的是内存地址0xb00,指向它所对应的内存空间
- 执行代码的时候,给AD里面的num:undefined等赋值,变成num:123等
- 执行第13行代码的时候,这里是调用了bar函数,然后它就会在函数调用栈中创建一个函数执行上下文
- 打印name的时候,先在AO里面找是否有name,没有-->去上层作用域找,没有-->沿着作用域链继续往上层找,找到了name = 'why',所以最终打印出来是'why'。
函数调用函数执行过程
打印结果:Hello Global
执行过程:首先,预编译 {message : undefined, foo:0xa00, bar:0xb00};
然后执行代码给各项赋值{message :"Hello Global", foo:0xa00, bar:0xb00};按照顺序,先执行bar(),那么就是先调用了bar() ,然后这里就有个函数执行上下文 ,这里面的VO对象是AO,在这里面var出来的message是储存在AO里面,接下来再调用foo(),又有一个函数执行上下文,这里执行的代码是打印message,那么首先找的是foo()里面有没有message,没找到就沿着作用域链向上查找,找到了父级中有个message,所以打印出来为"Hello Global"
变量环境和记录
- 其实我们上面的讲解都是基于早期ECMA的版本规范:
- 在最新的ECMA的版本规范中,对于一些词汇进行了修改:
- 通过上面的变化我们可以知道,在最新的ECMA标准中,我们前面的变量对象VO已经有另外一个称呼了变量环境VE。
作用域提升面试题
第一题
var n = 100;
function foo(){
n = 200
}
foo()
console.log(n)
输出结果:200
分析: 首先预编译->{n:undefined,foo:0xa00},然后编译时对n赋值,调用foo(),这里foo函数被调用就创建了一个函数执行上下文,这里执行代码n=200,然后n在foo函数对象里面找不到,就会到上层去找,找到父级里面的n,并赋值为200,所以就相当于修改了go里面的值,所以最后打印的时候,n为200。
注意:这里要区分一下,如果是这种情况,输出的n就是100
var n = 100;
function foo(){
var n = 200
}
foo()
console.log(n)
第二题
function foo(){
console.log(n)
var n= 200
console.log(n)
}
var n = 100
foo()
输出结果:undefined; 200
分析:
第三题
var n = 100
function foo1(){
console.log(n) //2、100
}
function foo2(){
var n =200
console.log(n) //1、200
foo1()
}
foo2()
console.log(n) //3、100
输出结果:见注释
第四题
var a= 100
function foo(){
console.log(a)
return
var a=100
}
foo()
输出结果:undefined
分析:
第五题
function foo(){
var a = b = 100
//这里相对于var a = 10
// b = 10
//b这里没有定义,js会默认把他放在全局
}
foo()
console.log(a)
console.log(b)
输出结果:undefined ; 10