进程与线程
在讲浏览器的事件循环机制,需要先讲一下浏览器的进程模型。
何为进程
每个程序的运行都需要内存开辟一块独属于它自己的空间,我们可以把这块空间理解为进程。
每个应用程序都至少有一个进程,每个进程之间相互独立,互不影响。
何为线程
一个进程至少包含一个线程,程序在进程开启后会自动创建一个主线程来执行代码,如果程序需要同时执行多块代码,则需要创建多个线程来同时执行,因此一个进程可以包含多条线程。
浏览器中的进程与线程
首先,浏览器是一个多进程多线程的程序。
为了避免相互的影响,浏览器在启动后会自动启动多个进程,其中,主要的进程包括:
-
浏览器进程
主要负责界面的显示,用户的交互,子进程的管理等,浏览器进程内部会启动多个线程处理不同的任务。
-
网络进程
负责加载网络资源,网络进程内部也会启动多个线程来处理不同的网络任务。
-
渲染进程
渲染进程启动后,会开启一个主渲染进程,负责执行HTML、CSS、JS代码。默认情况下,浏览器会为每个标签页开启一个渲染进程,以保证各个标签页之间互不影响。
在chrome官方文档中,将来的模式可能会改变,chrome在将来计划为每个站点开一个进程,这里不过多讨论,感兴趣的可以自行阅读chrome文档。
从chrome的任务管理器中可以很清楚的看到浏览器开启的进程。这里我们主要讲渲染进程。
渲染主线程的工作机制
事件循环
渲染主线程是浏览器中最繁忙的线程,它需要处理的任务包括解析HTML、解析CSS、计算样式、处理图层、执行全局js代码、执行事件处理函数、执行计时器回调函数等等,要处理这么多任务,渲染主线程就遇到了一个难题:如何调度任务?
渲染主线程是这么解决的:
渲染线程在执行全局代码时,会开启一个消息队列,来存放其他的任务,这些任务可以来自主线程正在执行的任务易,也可以来自其他线程产生的任务,包括但不限于计时器回调函数,事件处理函数等等。具体的执行步骤分为以下三步:
- 在最开始的时候,渲染进程会进入一个无限循环。
- 每一次循环完成会检查消息队列中是否有任务存在如果有,就取出第一个任务放入主线程来执行,执行完则进入下一次循环,如果没有则进入休眠状态。
- 主线程和其他所有线程(包括其他进程的的线程)可以随时向消息队列里面添加任务,再添加完一个任务之后,会将休眠状态的主线程唤醒以继续循环来从消息队列拿取任务执行。
以上步骤可以用一段伪代码来解释:
javascript
// eventLoop是一个用作队列的数组
// (先进,先出)
let eventLoop = [];
let event;
// 无限循环
while (true) {
// 如果消息队列不为空
if (eventLoop.length > 0) {
// 拿到队列中的下一个任务
event = eventLoop.shift();
// 执行下一个任务
try {
event();
} catch (err) {
reportError(err);
}
}
}
整个过程被称为事件循环,事件循环中的循环就是这么来的!
异步执行
代码在执行过程中,会遇到一些无法立即处理的任务,比如:
- 计时完成后才需要执行的任务(
setTimeout
、setInterval
) - 网络通信完成后需要执行的任务(
XHR
、Fetch
) - 用户交互后执行的任务(
addEventListener
)
如果让渲染主线程等待这些任务执行,就会导致主线程长期处于阻塞 状态,从而导致浏览器卡死。
因此浏览器采用异步执行的方式来解决这个问题:
js是一门单线程语言,是因为它运行在浏览器的渲染主线程里面,而渲染主线程只有一个,当渲染主线程遇到无法立即执行的任务时,主线程会将此任务交给其他线程去处理,从而保证自己不会阻塞,当其他线程中的任务执行完后会将回调函数包装成任务放入消息队列末尾,等待主线程执行。
队列和任务有优先级吗?
首先,任务没有优先级,在消息队列中先进先出,即先放入的会先执行。
但是,消息队列是有优先级的,也就是说消息队列不止一个!
根据W3C的解释:
- 每个任务都有一个任务类型,同一类型的任务必须在同一队列,不同类型的任务可以在不同队列。
- 每一次事件循环,渲染线程会从不同的=队列中取出任务执行
- 此外浏览器必须准备好一个微队列(micro queue),并且微队列中的任务优先于其他队列
在目前的chrome中,至少包含以下队列:
- 微队列:存放需要最优先执行的任务(Promise、MutationObserver等) -- 优先级最高
- 交互队列: 存放用户进行交互后执行的任务 --优先级高
- 延时队列:计时器到达后执行的任务 -- 优先级中
javascript
// 把函数fn放入微队列
Promise.resolve().then(fn)
// 把函数fn放入交互队列
addEventListener('click', fn)
// 把函数fn放入延时队列
setTimeout(fn, 1000)
讲到这,整个浏览器的事件循环机制就讲完了,我们可以通过一个例子检验一下自己有没有真正的理解:
javascript
function a() {
console.log(1);
Promise.resolve().then(function () {
console.log(2);
});
}
setTimeout(function () {
console.log(3);
}, 0);
Promise.resolve().then(a);
console.log(5);
通过画图来分解一下:
答案就是 5 1 2 3
自此结束。