# 从浏览器->V8->JS的执行过程
在了解浏览器运行机制、v8运行机制、JS执行上下文等模块后,我不禁产生了一个想法,那是不是可以把他们串联起来,了解大概流程呢?那岂不是要从盘古开天辟地开始写起....
其实不然,我是无敌缝合党,我站在巨人的肩膀上,自然不用从冯诺依曼讲起。 下面请欣赏,领域展开!
# 第一步:从操作系统到浏览器
在一个天气晴朗的周末,你想打开网站学习一会,那么这个过程发生了什么呢?
- 你打开了浏览器,操作系统为浏览器创建了一个进程,为进程分配内存等资源,然后加入进程队列中执行。于是你就看到浏览器的UI界面了。
- 熟悉的导航栏和tab跃然眼前,导航栏是浏览器线程控制界面线程(ui thread)提供的,同时浏览器线程还为你打开了新的tab,这是一个单独的渲染进程(renderer process)。
- 处理输入:你迫不及待开始学习了,然后你输入了网址,浏览器进程的界面线程为你判断是转到网址还是搜索引擎。
- 开始导航:当你点击 Enter 键时,界面线程会发起网络调用来获取网站内容。标签页的一角会显示“正在加载”旋转图标,并且网络线程会通过适当的协议(例如 DNS 查找)并为请求建立 TLS 连接。(网络这里可以拓展tcp连接过程,不属于本文重点讨论内容就不赘述了,在其他篇幅中见)
- 处理响应:网络线程会查看响应流的响应头:如果响应是 HTML 文件,下一步就是将数据传递给渲染程序进程;但如果它是 ZIP 文件或其他某个文件,则意味着它是一个下载请求,所以它们需要将数据传递给下载管理器。
- 查找渲染进程:在完成所有安全检查且网络线程确信浏览器应导航到所请求的网站后,网络线程会告知界面线程数据已准备就绪。然后,界面线程会找到渲染程序进程来继续渲染网页。
- 提交导航:现在数据和渲染程序进程已准备就绪,浏览器进程会向渲染器进程发送 IPC 以提交导航。它还会传递数据流,以便渲染程序进程可以继续接收 HTML 数据。浏览器进程听到在渲染程序进程中发生提交的确认信息后,文档加载阶段随即开始,导航就完成了,此时访问记录也被记录在磁盘中。
- 开始渲染:提交导航后,渲染器进程会继续加载资源并渲染页面,具体看第二部分
- 完成渲染后:渲染程序进程“完成”渲染后,会将 IPC 发送回浏览器进程(在网页中的所有帧上触发所有 onload 事件并完成执行后)。此时,界面线程会停止标签页上的加载旋转图标。
以上就是浏览器大体流程,下一步我们开始分析如何渲染出页面的。
浏览器进程: 标签页以外的一切都由浏览器进程处理。浏览器进程具有如下线程:用于绘制浏览器的按钮和输入字段的界面线程、负责处理网络堆栈以从互联网接收数据的网络线程、用于控制文件访问的存储线程等。当您在地址栏中输入网址时,输入由浏览器进程的界面线程处理。参考 (opens new window)
进程有自己的专用内存空间,因此进程通常包含通用基础架构的副本(例如,V8JavaScript 引擎)
# 第二步:渲染进程处理网页
渲染进程处理网页内容。渲染程序进程的核心任务是将 HTML、CSS 和 JavaScript 转换为用户可与之互动的网页。参考 (opens new window)
- 解析:当渲染器进程收到导航的提交消息并开始接收 HTML 数据时,主线程开始解析文本字符串 (HTML),并将其转换为一个对象(DOM)。
- 网站通常使用图片、CSS 和 JavaScript 等外部资源。为了加快速度,系统会并发运行“预加载扫描程序”,将请求发送到浏览器进程中的网络线程。
- JS阻塞:当 HTML 解析器找到 script 标记时,它会暂停解析 HTML 文档,并必须加载、解析并执行 JavaScript 代码。因为JS内容可能会改变DOM
- 解析JS,这是V8的领域了,详见第三部分
- 样式计算:主线程解析 CSS 并确定为每个 DOM 节点计算出的样式。优先级:提供的CSS > 浏览器默认样式表
- 布局:布局是查找元素几何形状的过程。主线程会遍历 DOM 和计算出的样式,并创建包含 x y 坐标和边界框大小等信息的布局树。
- 绘制:在绘制步骤中,主线程遍历布局树,创建绘制记录。绘制记录是绘图过程的记录,就像是“背景优先,然后是文本,然后是矩形”。
- 合成:可将页面的各个部分分离成图层,单独将其光栅化,然后在单独的线程(称为合成器线程)中合成为页面。如果发生滚动,由于图层已经光栅化,您只需合成一个新帧即可。通过移动层和合成新帧,可以采用相同的方式实现动画
- 遍历布局树生成层树,layout-tree -> layer tree,根据布局树分出层次,构建层树
- 光栅化和合成帧:图层树创建完毕并确定绘制顺序后,主线程会将该信息提交到合成器线程。具体的:
- 合成器线程会光栅化每个图层:合成器线程将每个图层划分为图块,然被发送到GPU光栅线程。光栅线程会光栅化每个图块并将其存储在 GPU 内存中。
- 合成帧:合成器线程会按需创建合成器帧,然后通过 IPC 将合成器帧提交到浏览器进程
- 合成器线程优先对视口附近的内容进行光栅化和合成
- 光栅化后,合成器收集绘制四边形的图块信息(图块在内存中的位置,在页面中的位置;页面框架),以创建合成器帧。
- 对于浏览器界面更改,可以从界面线程添加另一个合成器帧。这些合成器帧会发送到 GPU 以在屏幕上显示。如果滚动事件传入,合成器线程会创建另一个要发送到 GPU 的合成器帧。
# 第三步:从渲染进程到JS引擎
一个浏览器可以有多个渲染进程。通常每个浏览器的标签都会有一个渲染进程,并初始化一个 V8 实例(JS引擎我们以V8举例) 渲染进程里的这些东西,我们也可以叫JS runtime:
Call Stack(调用栈)、Heap(堆)、Callback Queue(回调队列)、Event Loop(事件循环)、Web API 和 Web DOM
HTML 解析器在遇到 script 标签时,标签中的源代码会从网络(network)、缓存(cache)或者已经安装的 Service Worker 中加载。此时渲染线程会被挂起,等JS引擎线程执行结束后继续(互斥)
下面分析下,JS的解析过程:
- 初始化宿主环境的JS运行时、实例化V8(前置工作)
- 从网络或者缓存中加载JS代码
- 脚本代码脚本代码以字节流的形式被响应,然后由字节流解码器解码,分词。 字节流解码器从解码的字节中创建出许多 Token。比如: 0066 解码为 f
- 解析器和预解析器生成抽象语法树,确认作用域
- 解释器执行字节码
- 监听热点代码
- 优化热点代码为二进制的机器代码
- 如果收集优化错误,比如类型变化,则反优化生成的二进制机器代码
# 第四步-最终章:JS的执行过程
终于来到最终章了!
执行前准备:
- 首先,js引擎在执行代码之前会在在堆内存中创建一个全局对象GO(Global Object):
- 该对象在所有作用域可访问
- 会有 Date,Math,SetTimeOut,SetInterval,String,Array,Number等
- 内置window属性指向它本身
- JavaScript引擎会在内部创建执行上下文栈ECS(Execution Context Stack),用于执行代码调用。
开始执行:
- 程序启动,全局上下文被创建
- 创建全局上下文的 词法环境
- 创建 对象环境记录器 ,它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理 let 和 const 定义的变量)
- 创建 外部环境引用,值为 null
- 创建全局上下文的 变量环境
- 创建 对象环境记录器,它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
- 创建 外部环境引用,值为 null
- 确定 this 值为全局对象(以浏览器为例,就是 window )
- 创建全局上下文的 词法环境
- 函数被调用,函数上下文被创建
- 创建函数上下文的 词法环境
- 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理 let 和 const 定义的变量)
- 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
- 创建函数上下文的 变量环境
- 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
- 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
- 确定 this 值
- 创建函数上下文的 词法环境
- 进入函数执行上下文的执行阶段: 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值
← V8是如何执行JS的 执行上下文 →