浏览器渲染主线程与队列
浏览器的进程模型
要了解事件循环概念我们要先了解浏览器的进程模型。
浏览器的进程模型是指浏览器在执行任务时如何划分和管理进程,以实现页面渲染、网络请求、插件处理等功能。不同的浏览器使用不同的进程模型来优化性能、增强安全性和提高稳定性。
1. 单进程模型
在早期浏览器(如最早版本的IE浏览器)中,所有任务都在一个进程中完成。这种模型会将浏览器的界面、网络请求、JavaScript 解析和页面渲染都集中在一个进程里。
主要特点如下:
优势:占用较少的系统资源,适合低性能设备。
劣势:不安全、不稳定。一个页面崩溃或代码执行错误可能导致整个浏览器崩溃或卡死。
2. 多进程模型
现代浏览器(如 Chrome、Edge)多采用多进程模型。多进程模型将不同任务划分到独立的进程中,以减少任务之间的相互干扰。
一般来说,常见的进程类型包括:
浏览器进程:负责界面展示、用户交互、文件管理等功能。
渲染进程:负责页面渲染和 JavaScript 解析,通常每个页面或标签页使用一个独立的渲染进程。
插件进程:负责处理插件(如 Flash)内容,避免插件崩溃影响其他页面。
GPU 进程:负责图形加速任务,提升渲染性能。
这种模型下,浏览器进程是主进程,其他进程负责特定任务并与主进程通信。
优势:安全性和稳定性高。页面崩溃不会影响其他页面或浏览器进程。
劣势:资源占用较高,每个标签页和插件进程都消耗系统内存。
3. 混合进程模型
有些浏览器(如 Firefox)采用混合进程模型,介于单进程和多进程之间。通常会分为一个浏览器主进程和多个渲染进程,但不是每个标签页都独占一个进程,而是根据需要共享渲染进程。Firefox 的 Electrolysis
(e10s) 项目就是这种模型的实现。
优势:在提升性能和安全性的同时,内存占用更低。
劣势:相较于完全独立的多进程模型,某些页面崩溃可能会影响其他页面。
4. 站点隔离进程模型
站点隔离模型是 Chrome 等浏览器的进一步优化,主要为了防范不同站点间的跨站脚本攻击(如 Spectre 漏洞)。每个站点使用单独的渲染进程,同一站点的多个标签页可以共享进程,而不同站点的标签页则独立于不同进程中。
优势:安全性更高,可以更好地防止站点间的信息泄露。
劣势:资源消耗进一步提升,需要更多的内存来支持隔离。
何为进程
程序运行需要有它自己的专属的内存空间,可以把这块内存空间简单的理解为进程。
每个应用至少有一个进程,进程之间相互独立,即时要通信也需要双方同意(王者荣耀登陆微信授权);
何为线程
一个进程至少有一个线程,所以进程在开启后会自动创建一个线程来运行代码,该线程称之为主线程
。
如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。
以当前比较火的手游王者荣耀为例:
主线程:处理登录注册之类的
游戏线程:监听点击事件,响应用户操作
网络线程:看别人有移动,放啥技能
实际上游戏内部不止上述三个线程,以上只是为了举例来说明线程
渲染主线程是如何工作的?
了解完浏览器的基础知识后,我们来了解浏览的渲染主线程
渲染主线程是浏览器中最繁忙的线程。
需要他处理的任务包括但不限于:
解析 HTML 和 CSS
计算样式(例如,将
vw
、百分比等单位换算为像素)布局计算(确定每个元素的位置和尺寸)
图层处理(如处理
z-index
确定元素的前后顺序)以每秒 60 帧的速率刷新页面,以确保动画和交互效果流畅
执行全局 JavaScript 代码
执行事件处理函数
触发计时器回调函数
…等等
要处理这么多的任务,主线程遇到一个前所未有的难题:如何调度任务?
比如:
当正在执行一个 JavaScript 函数时,如果用户点击了按钮,是否应立即中断当前任务去处理点击事件?
当执行一个 JavaScript 函数时,某个计时器达到时间,是否应该立即执行其回调?
如果浏览器同时收到“用户点击按钮”的通知,而某个计时器也到时间,应该先处理哪一个?
渲染主线程使用队列或者白话一点就是排队来处理这些问题
检查消息队列:每次循环,渲染主线程会检查消息队列中是否有待处理任务。如果有,则从队列中取出第一个任务执行。完成后,主线程重新进入循环,继续检查下一个任务。
进入休眠:如果消息队列中没有任务,主线程会进入休眠状态以节省资源。
唤醒机制:其他线程(或其他进程中的线程)可以随时向消息队列添加任务。新任务会添加到队列末尾。当有新任务加入时,如果主线程处于休眠状态,则会被唤醒以继续循环处理任务。
通过这种队列机制,渲染主线程能够有效地按顺序处理任务,避免不同任务间的冲突或抢占。渲染主线程会根据消息队列的顺序处理事件,比如 JavaScript 函数、点击事件、计时器回调等,确保页面响应的连贯性和稳定性。
这种调度策略帮助浏览器主线程更高效地处理任务,同时确保页面流畅运行。
这整个过程被称之为事件循环(消息队列
)
异步编程
熟悉js编程的小伙伴都知道,我们在js进行进行的Api请求、事件绑定等等相关方法都是常常采用回调函数的方式来处理,亦或者代码在执行过程中,会遇到一些无法立即处理的任务。
比如:
计时完成后需要执行的任务 –
setTimeOut
、setInterval
网络通信完成后需要执行的任务 –
XHR
、axios
用户操作后需要执行的任务 –
addEventListener
如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于阻塞
的状态,从而导致浏览器卡死
。
渲染主线程承担的极其重要的工作,无论如何都不能阻塞。因此浏览器选择了异步来解决这个问题。
如下图:
1. 渲染主线程
起始点:流程图从“渲染主线程”开始,这是处理图形界面和用户交互的主要线程。
2. 计算开始
任务入队:在“计算开始”阶段,一个任务(例如,需要定时执行的操作)被创建并放入“message_queue”的消息队列中。任务现在等待被执行。
通知计时线程:同时,渲染主线程通知一个名为“计时线程”的单独线程开始计时。这个计时线程与渲染主线程并行运行,负责监控任务的执行时间。
3. 消息队列(message_queue)
任务等待:在消息队列中,任务按照先进先出的顺序等待被执行。队列是任务调度的核心,确保任务按照预定的顺序和资源可用性被执行。
4. 计算任务结束
任务执行:在某个时刻,消息队列中的任务被取出并执行。执行可能涉及数据处理、网络请求、文件读写等操作。
任务完成:一旦任务执行完毕,它会被从消息队列中移除,并可能触发一些后续操作,如更新用户界面、发送通知等。
5. 计时线程
计时开始:计时线程在接收到渲染主线程的通知后开始计时。
计时中...:计时线程持续监控任务的执行时间,直到任务完成或达到预定的时间限制。
计时结束:当任务完成或时间限制到达时,计时线程停止计时。如果任务完成了,计时线程可能会将一个回调函数放入消息队列的末尾,以便在稍后执行一些清理工作或更新状态。
任务有优先级吗?
任务没有优先级,在消息队列中先进先出。但消息队列是有优先级的
。
根据 W3C 的最新解释:
每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
浏览器必须准备好一个微队列,
微队列中的任务优先于所有其他任务执行
。
但随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法。
以目前 chrome 的实现举例, 优先级排序如下:
微队列:用户存放需要最快执行的任务,优先级「最高」
交互队列:用户存放用户操作后产生的时间处理任务,优先级「高」
延时队列:用户存放计时器到达后的回调任务,优先级「中」
但其实浏览器还有很多其他队列,由于与前端开发关系不大,此处不作说明。
添加任务到微队列的主要方式:Promise
、MutationObserver
。
Promise.resolve().then(函数); // 立即把一个函数添加到微队列
// 题目 5,4,3,1,2,6
function a() {
console.log(1);
Promise.resolve().then(() => {
console.log(2);
})
}
setTimeout(() => {
console.log(3)
Promise.resolve().then(a)
}, 0);
Promise.resolve().then(() => {
console.log(4);
setTimeout(() => {
console.log(6)
},0);
})
console.log(5);
面试常见问题
如何理解 JS 的异步?
JS 是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。渲染主线程承担的诸多的工作,渲染页面、执行 JS 等。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
阐述下 JS 的事件循环
事件循环是浏览器渲染主线程的工作方式。
在 Chrome 的源码中,它开启了一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列,不同的队列有不同的优先级,在一次时间循环中,由浏览器自行决定去哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
总的来说:单线程是异步产生的原因,事件循环是异步的实现方式。
当然此处还是说一句博主的观点:
虽然我本身不喜欢,也不提倡八股文背诵来面试,但这不代表你可以什么都不用了解,可以不熟记所有的知识,但是你不能不知道有这么个知识。你可以不了解具体的实现,但是你不能不知道有这么个知识,如果你都不知道有这么个东西,即使你遇到问题了,你连查找关键词都整不明白,既影响你的工作效率,也会让自己因无法解决问题而产生工作情绪。
- 感谢你赐予我前进的力量
本网站的原创文章部分资源内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主邮箱:zzyo.yj@outlook.com 进行删除处理
本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向博主举报
声明:版权所有,违者必究 | 如未注明,均为原创 | 本网站采用CC BY-NC-SA 4.0 协议进行授权
转载:转载请注明原文链接 - Lycoris