浏览器的进程和线程
何为进程
当启动一个程序时,操作系统就会给该程序创建一个内存空间(当程序被中止时,该内存空间就会被回收),该内存空间就是用来存放程序代码,运行中的数据和一个执行任务的主线程,这样的一个运行环境(内存空间)就被称为进程。
进程的特点: 进程与进程之间是
完全隔离,独立运行。一个进程崩溃不会影响其他进程,避免一个进程出错影响整个程序。进程与进程之间的通信,代价较大,需要借助进程通信管道 IPC来传递。
何为线程
线程是依附于进程的,所以在进程开启后会自动创建一个线程来运行代码,该线程被称为主线程。如果程序要同时执行多块代码,主线程就会启动更多的线程来运行,所以一个进程中包含多个线程。
线程的特点: 一个进程包含
多个线程,每个线程并行执行不同的任务。其中一个线程崩溃了,那么整个进程也就崩溃了。线程之间可以相互通信。
浏览器有多个进程和多个线程
浏览器是一个多进程架构设计,每打开一个标签页就会创建一个进程。当然浏览器内部也有着自己的优化,比如:当浏览器同时打开多个空白标签页的时候,会合并成一个进程。
浏览器到关闭到启动,至少开启四个进程:1 个
browser进程,1 个GPU进程,1 个网络进程,1 个渲染进程。默认情况下,每打开一个标签页,就会开启一个渲染进程。但是也会存在特殊的情况,就是如果打开的标签页在同一个站点下,会共享同一个渲染进程。
可以在浏览器的任务管理器中查看当前的所有进程。
其中最主要的进程:
browser进程:主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。网络进程:负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。渲染进程:渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。默认情况下,每打开一个标签页,就会开启一个渲染进程,以保障不同标签页之间互不影响。
渲染主线程
渲染主线程是浏览器中最繁忙的线程,它处理的任务包括且不限于:
- 解析 HTML、CSS
- 计算样式及布局
- 处理及渲染图层
- 执行 JS 代码及执行事件、处理函数等
那么,渲染进程为什么不使用多个线程来处理这些事情呢?
JavaScript的单线程性质:Javascript 最初设计为单线程语言就是为了避免并发问题和状态管理的复杂性。DOM的一致性:如果多个线程同时修改 DOM,可能会导致不可预测的结果。CSS的渲染规则:CSS 通常依赖于文档的顺序和嵌套关系。多线程可能会干扰这些规则,导致渲染不一致。事件循环:Javascript 的事件循环机制确保了异步操作和回调的顺序执行。引入多线程可能会导致事件循环的管理和调度变得更加复杂。浏览器安全模型:浏览器安全模型通常基于单线程模型设计。多线程可能会引入新的安全漏洞和跨站脚本攻击的风险。性能和资源管理:引入多线程会增加资源管理的复杂性。线程之间的通信和同步会引起额外开销。兼容性和标准:引入多线程会影响现有代码的兼容性,并需要新的 Web 标准和规范。开发者体验:多线程编程比单线程要复杂的多,加大了开发者的能力要求。
主线程任务调度
既然主线程只能是单线程,那么要处理这么多任务,主线程会如何调度呢?方法就是:排队,这个过程就叫做事件循环。
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查任务队列中是否有任务存在。如果有,就取出第一个任务执行,执行完之后进入下一次循环。如果没有,则进入休眠状态。
- 其他所有线程都可以随时向任务队列添加任务。新任务会加到任务队列末尾。在添加新任务时,如果主线程处于休眠状态,则会将其唤醒以继续循环拿取任务。
何为异步?事件循环与异步的关系
在代码执行过程中,往往会遇到一些无法立即处理的任务。例如:
- 计时完成后需要执行的任务 --------- setTimeout、setInterval
- 网络通信完成后需要执行的任务 --------- xhr、fetch、promise 等
- 用户操作后需要执行的任务 --------- addEventListener 等
如果让渲染主线程等待这些任务的时机到达,那么会导致主线程长期处于阻塞状态,从而导致浏览器卡死。
因此,浏览器通过 异步 来解决该问题。使用异步的目的就是让主线程永不阻塞,从而最大限度的保证了单线程的流畅运行。
任务是否有优先级
任务没有优先级,在消息队列中遵循先进先出原则。但是,消息队列是有优先级的。
根据 W3C 的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在同一队列;不同类型的任务可以分属于不同队列。在一次事件循环中,浏览器可以根据实际情况从不同队列中取出任务执行。
- 浏览器必须准备好一个微队列,
微队列中的任务优先级要高于其他任务队列。
目前,谷歌浏览器中至少包含下面几个队列:
- 延时队列:用于存放计时器到达之后的回调任务,优先级:中
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级: 高
- 微队列:用户存放需要最快执行的任务,优先级:最高
添加任务到微队列的主要方式一般通过 promise、MutationObserver
经典面试题
解释一下 JS 的事件循环
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在谷歌浏览器中,主线程是一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要将任务在合适时间从消息队列末尾加入。
根据最新的 W3C 规范:每个任务都有不同的类型,同一类型的任务必须在同一个队列,不同的任务处于不同队列。不同队列有不同的优先级,在一次事件循环中,浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,并且微队列的优先级最高,必须优先调度执行。
JS 中的计时器能否做到精确计时,为什么
不能做到,因为:
- 计算机硬件没有原子钟,无法进行精确计时。
- 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这种偏差。
- 根据 W3C 标准,浏览器实现计时器是,如果嵌套层级超过 5 层,则会携带 4 毫秒的最少时间。
- 受事件循环影响,计时器的回调函数只能在主线程空闲时间运行,因此又带来了偏差。
输出下面函数的结果
function a() {
console.log(1);
Promise.resolve().then(function() {
console.log(2)
})
}
setTimeout(() => {
console.log(3)
Promise.resolve().then(a)
}, 0)
Promise.resolve().then(function() {
console.log(4)
})
console.log(5)
输出结果: 5、4、3、1、2
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
输出结果:script start、async1 start、async2、promise1、script end、async1 end、promise2、setTimeout
async function a1() {
console.log("a1 start");
await a2();
console.log("a1 end");
}
async function a2() {
console.log("a2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("promise1");
});
a1();
let promise2 = new Promise((resolve) => {
resolve("promise2.then");
console.log("promise2");
});
promise2.then((res) => {
console.log(res);
Promise.resolve().then(() => {
console.log("promise3");
});
});
console.log("script end");
输出结果:script start、a1 start、a2、promise2、script end、promise1、a1 end、promise2.then、promise3、setTimeout
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
输出结果:3、7、4、1、2、5
注意:resolve(6) 不会生效,因为 p 这个 Promise 的状态一旦改变就不会在改变了。