浏览器的进程模型
何为进程?
简单的理解为正在运行的程序,程序的运行时空间是在内存中。每个程序至少都有一个进程,进程之间相互独立,但是也可以互相的通信。
何为线程?
一个进程至少有一个线程,如果一个进程中只有一个线程,那么该线程就叫做主线程。
主线程被干掉了,那么整个进程就结束了。
如果程序需要同时执行很多的代码,主线程就会启动更多的子线程来执行代码,提高程序的运行效率。
(上述内容需要深入学习的话,请参见 操作系统 章节)
浏览器有哪些进程和线程?
浏览器是一个多进程、多线程的应用程序。
为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。
我打开了我的Chrome,默认有一个空白标签页,然后调用系统的任务管理器查看的,证明了浏览器是一个多进程、多线程的模型。
(你也可以留心观察你的其他应用,会发现它们有的是单进程,有的是多进程)
(在 Chrome 中,调出 任务管理器 界面,就可以看到 Chrome 对应的进程了)
主要进程
- 浏览器进程:主要负责程序界面的显示、用户交互、子进程管理等。进程内部会启用多个线程来处理不同的任务。
- 网络进程:负责加载网络资源。进程内部会启用多个线程来处理不同的网络任务。
- 渲染进程:默认情况下,浏览器会为每一个标签页开启一个新的渲染进程,以保证不同的标签页之间不会相互影响(进程的沙箱隔离机制)。该进程默认只有一个主线程,即渲染线程。
(Chrome 将在未来改变这种模式,感兴趣可以去官网了解一下,需要搭梯子)
渲染线程是怎么工作的?
渲染线程非常的繁忙,需要处理的任务,包括但不限于:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 绘制(60 FPS)
- 执行 JS 代码
- ......
任务的调度问题
我们已经知道主线程需要干的活儿有很多,有很多的任务需要处理,那这些任务是如何调度的呢?
比如说,网络请求和用户点击时间同时发生,先处理谁?
浏览器采用了一种生产者-消费者模型进行解耦:
每一个渲染进程都有一个属于它自己的消息队列(任务队列),渲染主线程就负责从队列中不断的取任务执行,其他线程(网络通信、用户交互等)就负责往队列中添加任务。这样一来,渲染主线程就无须关心这些问题了,队列中先取到谁,就执行谁。这就是一个最基本的事件循环模型。
(队列是一种数据结构,不懂的同学可以参见 数据结构与算法 章节)
异步?
写前端的,最绕不开的就是这个话题了。一直在说异步非阻塞,那到底是一个什么东西呢?
在上文中,我们已经知道了,渲染线程要从消息队列中取任务执行,这里就涉及到一个问题了,如果某个任务比较耗时,比如说执行一个 setTimeout() 函数,延时时间设置为 3 s,如果采用同步的等待机制,那么渲染线程至少有 3 s的空窗期,不能执行任何任务,显然这样效率很低,而且交互性很差。所以,必须对某些API进行异步非阻塞的调用。
由此还要引入一个概念,事件驱动。我们都是知道js是一门事件驱动的语言,事件发生时,会执行对应的回调函数。但是,想一个问题,事件是不是需要监听,那这些监听的工作是谁来做的?其他线程来进行不同类型事件的监听,比如说计时线程就负责定时器的监听,当定时器的时间到了,封装对应的任务放入消息队列中,交给渲染线程去消费。其他事件也类似这样。渲染线程在执行这些任务的时候,如果是一个异步任务,比如说定时器、网络请求、Promise等,就会交给其他线程去执行,然后取出队列中的下一个任务执行。
JS阻塞渲染?
如果渲染线程执行的任务属于CPU密集型,比如说写了一个死循环,那么线程会在CPU上空转,或者说执行循环体中的代码,没办法执行渲染页面的任务,更别说消息队列中的其他任务。
(Chrome默认每秒渲染60帧,也就是每一16ms进行一次页面绘制,当然,不变的话,你绘制什么,肯定是要页面有变化嘛,比如说用户拖动了滚动条或者JS对DOM进行了操作)
任务优先级?
本质上任务是没有优先级的,队列遵守先进先出的原则。但是,随着互联网的不断发展,以前的宏队列和微队列已经不能满足现代网页的需求了。所以,W3C做出了一些新规定:
- 每个任务都有一个任务类型,相同类型的任务放入同一个队列中,不同类型的任务可以分属不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
- 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行。
(这是一种策略,具体的实现不同的浏览器或者相同浏览器不同的版本具体的实现可能不同)
这里会出现很多这种题目:给你一段代码,然后然你写出输出的顺序。如果你能很好的理解上文的内容的话,相信是没有问题的。
(做题的时候我们可以使用宏队列和微队列辅助)
function a() { // 函数不调用,不用看
console.log(1);
Promise.resolve().then(function() {
console.log(2);
});
}
setTimeout(function() { // 宏队列中添加
console.log(3);
Promise.resolve().then(a); // 执行到这里的时候,队列中没有其他任务了,相当于同步调用
}, 0);
Promise.resolve().then(function() { // 微队列中添加
console.log(4);
});
console.log(5); // 全局js,最先执行
- console.log(5); 输出 5
- 先执行微队列中的回调函数,输出 4
- 执行宏队列中的延时回调函数,输出 3
- 剩下的这些Promise封装,完全没有意义,输出 1,2
结果:5,4,3,1,2
其他的题目,像JS的计时器会什么不精确的问题,相信你肯定能靓到面试官。
2024.10.22
writeBy kaiven