V8是如何执行一段代码的
全局过程:如下图
第一步:生成抽象语法树(AST)和执行上下文
将源代码转化为抽象语法树,并生成执行上下文,这里主要说一下什么是抽象语法树,简称AST
为什么要转化为AST:高级语言是开发者可以理解的语言,但是让编译器或者解释器来理解就很困难,对于他们来说,能理解的就是AST,所以无论你使用的是解释型语言还是编译型语言,在编译的过程中,他们都会生成一个AST
如下面的代码:
1 var myName = " 极客时间 "
2 function foo(){
3 return 23;
4 }
5 myName = "geektime"
6 foo()
经过处理之后生成的AST结构如下图:
生成了AST抽象语法树之后,编译器或者解释器后续的工作都要依赖于AST,而不是源代码
AST的作用
:是一种非常重要的数据结构,有着广泛的应用。
1:其中最常见的就是babel
,babel是一个被广泛使用的代码转换器,可以将ES6代码转化为ES5代码,通过这样的方式可以解决有的浏览器无法ES6的语法的问题,而babel的转换原理就是先将ES6源码转化为AST,然后在将ES6语法的AST转化为ES5语法的AST,最后再利用ES5的AST生成JavaScript源代码
2:eslint也使用AST
,eslint是一个用来检查JavaScript编写规范的插件,其检测流程也需要将源代码转化为AST,然后再利用AST来检查代码的规范化问题
介绍完AST的作用,具体来看看AST到底是如何生成的
AST的生成过程
1:词法分析
,又称分词(tokenize),就是将一行行的源码拆解成为一个个token(指语法上不可能在分的,最小的单个字符或字符串)
2:语法分析
,又称解析(parse),作用是将上一步生成的token数据,根据语法规则转为AST,如果源码符合语法规则,则这一步就会顺利完成,但是如果源码存在语法错误,这一步就会终止,并且抛出一个语法错误
第二步:生成字节码
什么是字节码?
字节码就是介于AST和机器码之间的一种代码,但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码之后才可以执行
为什么要使用字节码?
之前没有字节码的时候,是直接将AST转换为机器码,在最初的时候,执行机器码效率很高,这种方式在发布一段时间内运行效果非常的好,但是随着Chrome在手机上的广泛普及,内存占用问题也暴露出来,因为V8需要消耗大量的内存来存放转换后的机器码,为了解决内存占用问题,引入字节码
第三步:执行代码
生成字节码之后,然后解析器对字节码进行解析
通常,如果有一段第一次执行的字节码,解析器会逐条解释执行,在执行字节码的过程中,如果发现有热点代码(一段代码被重复执行多次,称为热点代码),那么后台的编译器就会把这段热点代码的字节码编译为高效的机器码,当再次执行这段被优化的代码的时候,只需要执行编译后的机器码就行
字节码配合解释器和编译器是的这种技术,我们称为即时编译(JIT)
事件循环EventLoop
宏任务
在JS中,大部分的任务都是在主线程中执行的,常见的任务有:
1:渲染事件(解析DOM,计算布局,绘制)
2:用户交互事件(如鼠标点击,滚动页面,放大缩小等)
3:JS脚本执行事件
4:网络请求,文件读写完成事件
为了协调这些任务有条不紊的在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,比如延迟执行队列(
专门处理类似于setTimeout/setInterval这样的定时器回调任务
)和普通的消息队列,然后主线程采用一个for循环,不断的从这些任务队列中取出任务并执行任务,我们把这些消息队列中的任务称为宏任务
,所以setTimeout和setInterval是宏任务
微任务
引入微任务是为了解决异步回调的问题,常见的微任务有:
MutationObserver
、Promise.then(或.reject)
以及以Promise 为基础开发的其他技术(比如fetch API)
, 还包括V8 的垃圾回收过程
在每一个宏任务中定义一个微任务队列,当这个宏任务完成的时候,就回去检查微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务,执行完成采取执行下一个宏任务
EventLoop
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
执行过程:
1:刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入栈中执行,所以先打印start和end
2:setTimeout作为一个宏任务加入宏任务队列
3:promise.then作为一个微任务放入微任务队列
4:当本次宏任务执行完之后,会去检查微任务队列,发现有要执行的promise.then然后执行
5:执行完成之后进入到下一个宏任务setTimeout,继续执行
所以整个事件循环的过程为:
1:脚本作为第一个宏任务执行
2:执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列,
3:当前宏任务执行完成之后出队,然后去检查微任务队列,如果有的话就依次执行,知道微任务队列为空
4:执行浏览器UI线程的渲染工作
5:检查是否有web worker任务,有的话就执行
6:执行队中新的宏任务,回到2,依次循环