浏览器原理-v8引擎-js执行原理
js简介
js应用:
js的应用很广泛 可以应用于web,移动端,小程序,桌面应用,后端开发等
web开发包括(原生js,react,vue,angular等)
移动端开发(reactNactive,weex),reactNative是react的衍生物,用于移动端开发,其同样使用js语言
小程序端(微信/支付宝小程序,uniapp,taro)
桌面应用开发(electron),如vscode就是用electron开发的
后端开发(node),借助node环境和其中的一些框架,如express,koa,egg.js等可以开发后端应用程序
ts和js的区别:
ts给js带来了类型思维,ts源于js,最终归于js,ts是js的超集
js本身没有对类型进行限制,所以可能会对项目带来某种安全隐患
ts致力于为js提供类型检查,ts是基于js的,在其上面进行了一些扩展,最终ts运行的时候还是会转回js的,js如果加上了类型检测,ts就寄了
编程语言:
js是一门高级编程语言,与之对应的还有低级编程语言,从编程语言发展历史可以归为三个阶段,分别是机器语言,汇编语言,高级语言
机器语言:1和0组成的,如100101010101,一些机械指令
汇编语言:汇编指令
高级语言:c,c++,java,js
语言越高级越容易让人类理解,越低级机器越容易理解。计算机本身不认识高级语言,所以代码执行的时候是会从高级语言向下转换为机器指令才能真正运行起来的
高级语言的划分:
高级语言还可以划分编译型语言和解释性语言
编译型语言(c,c++,Java)直接将代码编译成可执行文件,这里java比较特殊,是先转换为字节码,再由字节码转换为机械码,js也有这个过程,该过程被称之为JIT
解释性语言(js,Python)边读代码边进行解释,解释后再进行执行
浏览器的工作原理
1,我们输入网站地址完成并回车后,dns(域名系统)会先把我们输入的网址进行解析,解析后的网址会变为ip地址,找到服务器地址后,服务器会返回一个index.html文件
2,然后浏览器会解析这个html,解析html的过程中发现用到了某css文件,就会回去到服务器中找到对应的css文件下载下来,同理,当遇到了某个script标签的时候就会下载对应的js文件,然后js代码才会在浏览器中运行。而不是在一开始就把js,css等资源一股脑都下载下来的
js在浏览器上跑,除了浏览器js还可以在node环境中运行(因为node环境中也有提供js运行的v8引擎)
浏览器内核
通过上述步骤下载了诸多文件(css,js)后,这些文件需要浏览器内核来帮助进行处理,浏览器内核最终把这些文件渲染成用户可以直观看到的界面,并提供交互等功能
不同的浏览器由不同的内核组成
Gecko:比较早的浏览器内核,Netscape和Mozilla firefox浏览器使用
Trident:微软开发的,但是如今edge已经转向blink
Webkit:来源的,苹果开发的,最早用于Safari 以前chrome也在用
Blink:基于webkit开发的分支,Google开发,目前应用于大多主流浏览器chrome,edge,opera等(现在基本上使用的都是Blink内核)
浏览器内核也称为浏览器的排版引擎(layout engine),也称为浏览器引擎/页面渲染引擎/样板引擎
浏览器渲染页面过程
1,再来看具体内核渲染文件的过程,首先进行文件的下载,率先被下载下来的是index.html(入口html文件),浏览器逐一解析该html内部的各个标签,浏览器中存在html-parser可以将html转换为dom树结构
2,在转为dom树结构的时候存在一个问题就是js的某些操作(如doucument.createElement),可能会对dom树产生修改。(这里就涉及到谁来执行js,因为js文件为高级语言,所以无法直接被计算机所识别,需要进行转换为计算机可以识别的机器语言,这一步就需要js引擎来执行)当浏览器解析html的时候遇到了js标签的时候,会停止解析html,而去加载和执行js代码,从而生成真实的dom树
3,同样样式文件也会经由css-parser解析,解析为css规则后会attachment到上面html解析所生成的dom树上
4,dom树与css规则相结合以后会生成一个渲染树(render tree),渲染树经过布局引擎进一步处理为(layout engine)进行真实布局(layout引擎与style rules所处理的操作有所区别,我理解这个layout引擎是做最终浏览器上显示出来的真实布局,当浏览器伸缩/尺寸发生变化时,对最终所呈现的页面进行一定的处理,如style rules里面规定了某个元素的在左下角的绝对定位,当浏览器缩放的时候,该绝对定位元素实际所呈现的位置也一定随之发生变化,这就是layout引擎所做的处理)最终每个元素所摆放的实际位置要经过layout引擎的布局,
5,经由布局引擎后生成最终的render tree并将其进行绘制(painting),最终展示(display)到页面上
js引擎:
上文提到了浏览器在进行渲染生成dom树的时候,可能会遇到js对dom元素进行修改的情况(如发送网络请求,修改dom树等),这里执行js代码的时候就需要用到js引擎
js是高级语言,所有高级语言最终需要转换为机器指令执行。无论是交由浏览器还是node环境所执行的js代码,最终都是cpu来执行的,cpu只认识自己的指令集,所以我们需要js引擎帮助我们将js代码翻译为cpu指令
简而言之,js引擎就是把js代码转换为机器语言交由cpu执行的工具
常见的js引擎:spiderMonkey,chakra,jsCore(苹果开发,小程序在用),V8(谷歌开发,市场上主流,node里面使用的也是V8引擎)
浏览器内核和js引擎的关系:
以webkit为例,webkit由两部分组成,WebCore和JSCore
WebCore负责html解析,布局,渲染等工作
JSCore则负责解析,执行js代码
v8引擎:
v8引擎简介,v8引擎是用c++编写的,开源的,高性能的js和webassembly(也称为WASM,是一种技术,旨在提高高级语言向机器语言转换并执行的效率)的引擎;v8引擎的作用是将高级语言js代码最终转化为可以让cpu执行的机器语言。
v8引擎应用于浏览器(chrome)和node.js中,可以在多个环境下运行,既可以独立运行,也可以嵌入到其他应用程序中(如node应用)
v8引擎的流程/原理
浏览器内核(Blink)解析html过程中遇到js代码时候会将js代码下载下来,下载完成后以流(stream)的形式将源码交给v8引擎,v8引擎中的scanner会将js代码解析成token(下文中词法分析的过程),接下来tokens会被转换为ast树,经过preparser和parser,parser将tokens转换为ast树架构,然后转换为ast结构
v8引擎的模块可以分为Parse/Ignition/TurboFan,主流程为parse->ignition,副流程turbofan
预解析(preparse)
正式开始parse之前先进行preparse预解析,js中大量代码有些代码是在刚开始运行的时候不需要被加载的,也就是不需要一开始被解析为ast结构的,针对这种情况v8引擎实现了延迟分析(lazy parsing)方案,作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行,比如在函数中定义了另一个函数,内部的函数在一开始就不需要解析(parse)的过程,取而代之的则是预解析(preparse),这样省去了所有js代码都一股脑儿转换为ast结构所进行的大量操作,提高效率
解析(parse)
V8引擎会将js代码首先进行parse(解析)处理,经过解析后会将js源代码转换为AST(抽象语法树);解析的过程包括了词法分析/语法分析
词法解析
1,首先进行词法分析,词法分析对代码中的每一个词进行一个切割,切割后最终生成若干个tokens,每个tokens是一个数组结构,每一个数组中存放很多对象:如const name=’xxx‘,词法分析的过程中,const就会被解析成token中的一个键值对,{type:‘keyword’,value:‘const’},类型为“关键字”,属性值为“const”;name会被解析为token中的另一个键值对,{type:‘identify’,value:‘name’},类型为“标识符”,属性值为“name”
语法解析
2,词法分析后token中就生成了多个对象,且这些对象被划分为了不同的标识(keyword/indentify等);然后进行语法分析,语法分析将词法分析产生的token,按照某种给定的形式文法转换生成ast(抽象语法树),可以在网站看自己的代码转换成ast树以后的样子。抽象语法树的应用非常广泛,如使用bable将ts转js(ts需要先转换为ast,对ast做修改生成新的ast,再生成代码最终转换为js),再比如vue里面的模板(template)转换也用到了ast
解释/转化(ignition)
转换过的抽象语法树更加容易操作。通过ignition转换为字节码,之所以转换为字节码,而不是直接转换为可供cpu运行的机器码,是因为v8引擎不能确定当前运行环境的cpu架构,不同cpu架构能执行的机器指令存在差异。
所以先预先转换为通用的字节码进行过渡,字节码最大的好处就是其可以跨平台使用。最终执行的时候会将字节码转换汇编语言,再转换为机器码。浏览器中的js引擎将js代码转换成了机器语言然后再cpu中运行。
这样一来一个流程便完成了,从原始js代码进行解析,转化后,js代码就可以被cpu所识别并执行,执行后的操作就会在浏览器中体现(如执行添加/删除dom的操作,执行后会在浏览器中重新渲染dom)。
编译(TurboFan)
该流程存在一个问题,每次字节码都要转换为汇编再转机器码运行,频繁执行时会影响性能,每次执行都需要进行转换,例如我们定义一个函数,每次执行该函数的时候,按照上面流程(原始js->parse->ignition),需要反复将该函数转化的字节码转换为汇编语言,再转为机器码运行,如此往复,导致字节码和汇编码之间的转换效率很低。
TurboFan这个库用于收集信息,记录下需要多次执行的函数,并将其进行标记为热函数(hot),并将此函数在当前环境下所对应的字节码保存下来,如果函数只需要调用一次的话则没必要进行保存,当再需要调用该函数的时候,直接使用保存下来的字节码即可,这样既可省去了汇编语言过渡的流程,一旦被标记为热函数,就在执行的时候直接将其从字节码转换为优化过的机器码,提升性能。
但是直接将字节码转换为机器码的操作会导致另一个问题。当被转为机器码的函数发生改变的时候(如两数相加的函数,正常传两个数字相加肯定没问题,转换为机器码以后,忽然往里面传入字符串(js是动态语言,不会对传入的参数做类型检测),字符串相加做拼接,数字相加做计算,两种不同的操作对应的cpu指令也不同,此时被转为做数字累加的机器码在做字符串拼接的时候肯定就不能用了)
这个时候会触发逆优化(Deoptimization),一旦在执行机器码的时候发现执行操作不一样了,就会再将机器码转换为字节码再进行执行
从这项特性我们也可以发现,在调用函数的时候尽可能传入相同类型的参数,这样v8引擎可以帮助我们做优化(通过turboFan),当传入参数出现变化的话v8引擎底层会触发操作浪费性能,ts由于在编写的时候需要规定类型,所以在v8引擎编译的时候效率要比js更高
一些补充:
parse模块会将js代码转换为ast,因为解释器并不直接认识js代码,需要进行转换
ignition是解释器,会将ast转换为bytecode(字节码),同时会收集turbofan优化所需要的的信息,如函数参数的类型信息,有了类型才能真实的运算,如果函数只调用一次,则ignition会执行解释bytecode
turbofan是编译器,可以将字节码编译成cpu可以执行的机器码,如果一个函数被多次调用就会被标记为热点函数,热点函数会经过turbofan转换为优化的机器码,提高代码的执行性能,机器码也有可能被还原成字节码,如果后续执行函数的过程中,类型发生了变化,之前优化的机器码不能正确的处理运算,就会逆向再转换为字节码
初始化全局对象
在js代码解析(v8的parse流程)的时候,也就是js源码转换为ast抽象语法树的过程中,js引擎会在堆内存创建一个全局对象GO(global object)
全局对象(GO,global object)
GO里面包含了一些属性和一些方法(如定时器等)。GO中有window属性,属性值为自己本身(GO)
同时在解析过程中,除了全局对象GO的创建以外,我们自己编写的js代码,(如一个声明某个变量为xxx,var name=‘xxx’),我们声明的name属性也会加入到GO中,此时只在GO中声明有这个name变量,但不会将name属性的值也添加进入name,所以此时GO中的name属性的属性值为undefined,解析完成后运行代码,为了执行代码,v8引擎内部会创造一个存在一个执行上下文栈(execution context stack),也称为调用栈。
执行上下文栈/调用栈(ECStack)
所有代码在运行的时候都需要先从磁盘加载到内存里,再在内存里转换为机器指令,最终在cpu上运行。也就是代码想要加载首先必须进入到内存里,一般会对内存划分结构(栈结构/堆结构)。代码只要想运行都必须先进入执行上下文栈,所有代码在执行的时候都需要进入栈里面执行,执行后出栈。
全局执行上下文(GEC,global execution context):**
当我们需要执行全局代码时候,为了能够正常的执行,需要创建全局执行上下文(global execution context)。全局代码需要被执行的时候GEC才会被创建,创建出来的GEC会被放进ECStack。全局执行上下文(GEC)在代码准备执行的时候会产生一个变量对象VO(variable object),这里的VO指向GO,也就是GO和VO在全局执行上下文 中是等价的,但并不代表他们永远相同(不同的代码中VO对应的指向不同,如函数中的VO指向的就是AO)
所有前置准备工作1,创建执行上下文栈ecstack,2,创建全局执行上下文gec,3,gec内部产生VO;完成后,才会真正开始执行代码
变量对象(VO,variable object)**:
变量对象存放的是函数或者全局代码执行过程中需要用到的局部变量
变量提升
继续拿上面那句var name=’xxx‘举例;上文中我们提到了,js代码进行解析的时候,会将我们编写的代码,也就是var name=‘xxx’这个操作进行编译,编译将name存入GO中,并且此时GO中name的属性值为undefined,而当解析完成后开始真正执行代码的时候,为name赋值为’xxx’的操作就是实际替换掉gec中的VO(此时的VO就是GO,实际指向的也就是GO中name属性,将其属性值进行修改),这时GO中的name就从undefined被修改为了’xxx’
总结
最后梳理一下,当代码在解析的时候,会创建GO,GO中会记录js文件中所有的变量,但是只做到知道该变量存在,也就是知道有这么个东西,并不会更深入的了解(为其赋值等操作),原因是为了尽可能减少将大量js代码都转换为ast结构所需要的大量消耗,在解析完成后,全局代码为了执行会先创建全局执行上下文gec,随后gec中会生成变量对象,全局代码中生成的gec中的VO指向GO,最后完成了这些操作后,js才开始真正执行,并把变量的实际值赋给GO中声明的变量,该赋值操作是从上到下一行一行进行的,而如果在某个变量实际赋值之前就访问其属性值,得到的结果就是undefined,从流程上来看,js真正执行前所做的准备工作(生成GO)就可以访问到属性值为undefined的变量(如上文提到的name),该过程就叫做变量提升