前言
作为一个前端工程师,少不了每天跟浏览器打交道,跟浏览器打交道的媒介就是通过JS,那么JS是怎么被浏览器解析的呢,这个时候,背后的V8引擎就在默默的为浏览器做这件事情,V8引擎具体会做那些事情呢?
客户端请求网页
当客户端请求网页时,服务器首先会返回一个index.html(大多数情况下都是叫index.html,但也有可能叫main.html),当浏览器拿到这个请求的html文件后会解析html文件,随后会根据html文件引入的css和js地址,去请求js文件和css文件。
以webkit为例,事实上webkit由两部分组成
- WebCore:负责html解析、布局、渲染等等相关的工作。
- JavascriptCore:解析、执行JavaScript代码。
所以html和css文件是由WebCore去解析并生成render tree 的
生成Render tree的过程
可以看到浏览器解析HTML文件的时候会将HTML转换生成一个DOM tree 但是这个DOM tree会受到JS的DOM操作影响,所以解析HTML的时候如果遇到JS代码则会先解析JS代码(JS代码则由JavasciptCore解析),也可以通过设置scirpt标签熟悉改变解析JS代码的时机(defer和async)。
defer和async都是用于控制浏览器在解析HTML文档时如何处理script标签的属性。它们的主要区别在于:
- defer属性:告诉浏览器该脚本将在文档解析完毕后执行,但在DOMContentLoaded事件触发之前执行。多个defer脚本会按照它们在文档中出现的顺序依次执行。这个属性只适用于外部脚本,或带有src属性的内部脚本。
- async属性:告诉浏览器该脚本将与文档同时解析和执行,不会阻塞HTML的解析。多个async脚本的执行顺序无法保证。这个属性只适用于外部脚本,或带有src属性的内部脚本。
解析CSS时,会将CSS转换成CSSOM规则,随后会将DOM tree和CSSOM tree 合并生成一个Render tree 随后进行布局,渲染展示。 这里需要注意,布局改变会影响Render tree,所以回流的时候会改变Render tree,但是重绘不会。
解析JS代码
这里我就以V8引擎为例,V8引擎是用来解析JS代码,JS代码运行时所需要的环境,浏览器和node中都存在V8引擎。
根据上边的流程图可以看出,当浏览器拿到javascript代码后先会进行parser,在这个环节会对JS代码进行语法分析和词法分析然后生成AST抽象语法树,随后AST抽象语法树会经过Ignition转换成字节码。
这里为什么会生成字节码呢?这就是浏览器跨平台的关键之处,浏览器之所以能够在windows和mac上跨平台使用就是因为Ignition会将AST抽象语法树先转换成字节码,然后再由字节码转换成汇编再转换成对应的cpu指令,因为各个系统平台的cpu指令是不一样的,所以我们不能直接的去转换成cpu指令,需要先转换成字节码,然后再根据运行的系统去转换对应平台的cpu指令(这里有点像Java的JVM的味道)。
但是,所有的代码都通过字节码去转换成对应的机器码(cpu指令)肯定会造成冗余的性能问题,所以就会出现TurboFan。
比如现在有一个这样的函数
js
function foo(){
console.log('1in')
}
这个函数会执行很多次,那我们每次执行都需要去用字节码转换的话是不是会浪费很多性能,我们只需要将他转换的机器码保存下来,下一次调用的时候直接拿来机器码调用就好了,所以Ignition会在解析AST抽象语法树的时候,会将多次执行的函数标记成hot函数告诉TurboFan,hot函数转换的机器码就会被保存下来,当下一次调用时直接调用保存下来的机器码,不会再一次通过字节码转换。
但是,如果hot函数的参数类型发生改变,就会重新生成字节码进而重新生成机器码,不会复用上一次缓存的,比如
js
function sum(value1,value2){
return value1+value2
}
//如果是这样的数字类型相加的函数连续复用的话,机器码会复用
const reslut1=sum(1,2)
const result2=sum(10,20)
//但是如果中途有这样的一个函数
const result3=sum('1','in')
像上边所提到的result3就不会复用之前的缓存的机器码,因为之前缓存的是数字类型的相加,而result3的相加是字符串的拼接,区别很大,所以需要重新生成字节码。
从上边可以看出,我们如果用TypeScript编写代码的话,约束了函数的入参类型后,无形中就会增加V8解析执行JS代码的效率。