事件循环
JavaScript运行在单线程中,依靠事件循环来管理异步操作。事件循环持续检查是否有任务完成并准备好被处理,如此循环确保了即使JavaScript是单线程的,它也能有效地处理异步操作。在介绍事件循环之前,我们来看看浏览器的一些基础知识。
浏览器的进程模型
主流浏览器(如Chrome、Firefox、Safari)的进程模型组成部分:
- 浏览器主进程(Browser Process) :
- 管理用户界面、地址栏、书签、后台标签页、文件下载等。
- 协调其他进程的活动。
- 渲染进程(Renderer Process) :
- 每个标签页通常有自己的渲染进程(在Chrome中称为沙盒化)。
- 负责网页的渲染,包括HTML解析、CSS样式应用、JavaScript执行等。
- GPU进程:
- 用于处理GPU任务,如3D CSS效果、硬件加速的页面合成等。
- 优化渲染性能和动画。
- 插件进程(Plugin Process) :
- 独立运行插件内容,如PDF查看器、Flash(现已淘汰)等。
- 每个插件一般运行在自己的进程中。
- 网络进程(Network Process) :
- 处理所有网络活动,如网页加载、文件下载等。
浏览器的线程模型
在这些进程中,会有多个线程执行具体的任务,例如:
- UI线程(在浏览器主进程中) :
- 处理用户界面的绘制和交互。
- 渲染线程(在渲染进程中) :
- 解析HTML、CSS,执行JavaScript,渲染网页内容。
- 通常每个标签页有自己的渲染线程。
- JavaScript引擎线程:
- 执行JavaScript代码。
- 在渲染线程中,与UI绘制互斥(因为JavaScript是单线程的)。
- 事件处理线程:
- 处理事件监听和回调函数。
- 网络线程:
- 在网络进程中处理HTTP请求和响应。
- GPU绘制线程:
- 在GPU进程中处理图形和文本的渲染。
- 定时器线程:
- 处理定时器设置和回调(如setTimeout和setInterval)。
- 独立的工作线程/Web Workers:
- 执行不影响UI渲染的后台任务。
浏览器渲染主线程工作步骤
浏览器的渲染主线程负责处理网页的大部分关键任务,包括解析HTML、CSS、JavaScript执行、布局、绘制等。这个线程的工作流程至关重要,因为它直接影响到用户的感知性能和页面响应能力。以下是渲染主线程工作的更详细描述:
1. 解析HTML生成DOM树
- HTML解析:当浏览器加载HTML文件时,渲染主线程开始逐行解析HTML文档,并将标签转换成DOM节点。
- 构建DOM树:这些DOM节点被组织成一种树状结构,即DOM树,它反映了文档的结构。
2. 解析CSS生成CSSOM树
- CSS解析:同时进行的是CSS的解析。这包括外部链接的CSS文件和页面内的样式标签。
- 创建CSSOM树:解析后的CSS构成CSS对象模型(CSSOM)树,它表示CSS样式的层叠和继承规则。
3. 构建渲染树
- 结合DOM和CSSOM:渲染主线程将DOM树和CSSOM树结合起来,生成渲染树。
- 渲染树内容:渲染树只包含需要显示的元素及其样式信息。隐藏的元素(如
display: none)不会包含在渲染树中。
4. 布局(Layout/Reflow)
- 计算元素位置:根据渲染树,计算每个元素在设备视口内的确切位置和大小。
- 响应布局变化:布局过程是响应性的,它会根据视口大小的变化和元素内容的变化重新计算元素的位置。
5. 绘制(Paint)
- 绘制像素:根据计算出的布局信息,渲染线程将元素绘制到屏幕上,这涉及将文本、颜色、图像、边框等转换为像素。
- 分层和合成:对于复杂的布局和动画,浏览器会将页面分成多个图层,然后合成这些图层,以优化性能。
6. JavaScript执行
- JS引擎:JavaScript代码由浏览器的JS引擎执行,它是渲染主线程的一部分。
- 影响DOM和CSSOM:JavaScript可以修改DOM和CSSOM,但这可能会触发布局和重绘,影响性能。
7. 事件处理
- 用户交互:处理来自用户交互的事件,如鼠标点击、滚动、键盘输入等。
- 事件监听器:执行绑定到这些事件的JavaScript事件监听器。
8. 异步操作和队列
- 任务队列:JavaScript的异步操作,如
setTimeout、setInterval、异步请求等,被放置在任务队列中,等待主线程空闲时执行。 - 事件循环:渲染主线程会周期性地检查任务队列,并执行队列中的任务。
事件循环
JavaScript运行在单线程中,依靠事件循环来管理异步操作。事件循环持续检查是否有任务完成并准备好被处理,如此循环确保了即使JavaScript是单线程的,它也能有效地处理异步操作。
让我们看看事件循环是如何管理异步操作的:
1. 主线程执行同步任务
- 当JavaScript引擎开始运行时,首先在主线程上执行脚本中的所有同步任务,包括变量赋值、函数调用等,都在主线程上按顺序执行。
- 这些同步任务按照它们在代码中出现的顺序,被推入调用栈(Call Stack)并执行。
2. 异步任务进入任务队列
主线程执行完同步代码后,会通过事件循环处理异步事件和回调。
- 当遇到异步操作(如
setTimeout、Promise、DOM事件等)时,它们的回调不会立即执行。 - 这些异步操作的回调函数会被放入不同的任务队列。根据异步操作的类型,它们可能被放入宏任务队列或微任务队列。
3. 宏任务(Macro Tasks)
- 宏任务包括:
setTimeout、setInterval、I/O操作、UI渲染等。 - 在每次事件循环的迭代中,至多执行一个宏任务。
- 当调用栈清空后,事件循环会从宏任务队列中取出一个任务执行。
4. 微任务(Micro Tasks)
- 微任务包括:
Promise.then、Promise.catch、Promise.finally、process.nextTick(Node.js中)、MutationObserver等。 - 在当前宏任务执行完毕后,所有的微任务都会被执行。
- 微任务队列会在移动到下一个宏任务之前完全清空。
5. 事件循环的循环
- 完成当前宏任务后,事件循环会检查微任务队列,执行里面的所有微任务。
- 执行完所有微任务后,如果有必要,浏览器可能会进行UI渲染。
- 然后事件循环会移动到下一个宏任务,重复同样的过程。
6. 事件和回调
- 在整个过程中,事件(如用户点击、网络请求返回等)可能会触发,并将它们的回调函数放入适当的队列等待执行。
示例
假设有以下代码:
console.log('同步任务开始');
setTimeout(() => {
console.log('宏任务');
}, 0);
Promise.resolve().then(() => {
console.log('微任务');
});
console.log('同步任务结束');
执行顺序将会是:
- 打印"同步任务开始"和"同步任务结束"。
- 执行微任务,打印"微任务"。
- 执行宏任务,打印"宏任务"。
详细的执行顺序
- 执行栈(同步任务) :
- 首先,执行栈(主线程)上的同步任务会被执行。这包括你的代码中所有不是在回调函数、Promise、定时器或任何异步API中的代码。
- 示例中,
console.log('同步任务开始');和console.log('同步任务结束');都是同步执行的。
- 微任务队列:
- 一旦执行栈中的同步任务完成,事件循环会立即处理微任务队列。
- 微任务队列包括由Promise产生的任务(例如
Promise.then())和其他API(如MutationObserver)的回调。 - 微任务队列会被完全清空,也就是说,所有的微任务都会被连续执行,即使在执行微任务的过程中又产生了新的微任务。
- 宏任务队列:
- 完成所有微任务后,事件循环会处理一个宏任务。
- 宏任务包括
setTimeout,setInterval, I/O 操作等的回调。 - 每次事件循环迭代只处理一个宏任务,然后再次检查微任务队列。