本篇文章将包括以下知识点:
- 在浏览器当中进程和线程的基本概念
- 浏览器当中的渲染进程及其主线程
- 浏览器当中事件循环的内容。
进程和线程是什么?
- 进程:进程就好比工厂的车间,它代表
CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。每个应用至少有一个进程,进程之间相互独立。 - 线程:线程就好比车间里的工人,一个进程可以包括多个线程。⼀个进程⾄少有⼀个线程,所以在进程开启后会⾃动创建⼀个线程来运⾏代码,该线程称之为主线程
每一个进程都会创建属于自己的内存空间,而进程之间的内存空间是共享的,线程之间是可以相互通信的。可以简单理解为每一个车间当中的每一位工人其实都可以进出,相互交流。
浏览器中的进程与线程
众所周知,浏览器本身是十分复杂的,它是一个多进程多线程的应用程序,浏览器现阶段最为重要的几个进程大致为
- 浏览器进程:主要负责页面展示逻辑, 用户交互,包括地址栏、书签、后退和前进按钮。还处理 Web 浏览器的不可见的特权部分,例如网络请求和文件访问。
- GPU进程:负责整个浏览器界面的渲染,独立于其他进程,专用于处理
GPU任务。 - 网络进程:负责加载⽹络资源。⽹络进程内部会启动多个线程来处理不同的⽹络任务
- 插件进程:控制网站使用的任何插件,例如 flash。
- 渲染进程:负责控制显示tab标签页内的所有内容,核心任务是将
HTML、CSS、JS转为用户可以与之交互的网页。默认情况下,每开启一个tab标签就会开启一个渲染进程,保证每个页面互不干扰。
渲染主线程是什么?
刚才我们提及到了浏览器当中存在一个渲染进程,每个tab标签会开启一个渲染进程,而在这个渲染进程当中,存在着多个线程,包括JS引擎线程,GUI渲染线程,网络线程,等等线程。在这么多线程里,我们一般把JS引擎线程和GUI渲染线程称之为主线程。
- 主线程常常负责处理用户输入、操作 DOM 和执行 JavaScript 等任务
- 其他线程用于网络请求、样式计算、布局等任务。
- 这些线程都在后台运行,并与主线程进行通信和协调,以保证浏览器的稳定性和性能。
主线程具体的工作
对于主线程来说,他需要做的事情是繁多的
- 解析和加载
HTMLCSS以及JavaScript文件 - 构建
DOM和CSSOM树 - 布局和绘制界面
- 处理用户输入和交互
- 执行
JavaScript脚本 - .....
从以上内容不难看出,对于主线程来说,处理这么多的任务。主线程不会卡顿和阻塞吗?该怎么调度这些任务?比如一个JS函数在执行过程中有一个定时器时间到了,或者用户触发了一个点击事件,哪些事件会先执行呢?是继续执行JS函数,还是直接不管JS函数去执行定时器的回调函数?
- 下面我举个简单的例子
<body>
<button id="btn" style="margin: 30px;">点击修改按钮里的文字</button>
<script>
const btn = document.getElementById('btn');
// 监听按钮的点击
btn.addEventListener('click',()=>{
// 记录点击时的时间,单位毫秒
const now = Date.now();
btn.innerHTML='修改了 !!!!'
// 执行一个死循环,两秒之后才会结束
while(true) {
if(Date.now() - now >= 2000){
console.log('2s 结束了');
break;
}
}
})
</script>
</body>
在上面的代码里,点击按钮,按照正常的逻辑想法,肯定是点击后,按钮的文字被修改后渲染到页面。然后执行死循环代码,最后打印。但事实上却不是这样的。
从实际效果中我们不难得出结论,当点击按钮之后,JavaScript 先执行了一个while循环。而这个while循环很明显阻止了其他任务的执行,包括更新页面上的button元素的innerHTML属性,导致无法立即改变文本内容。所以当循环结束并打印“2s 结束了”后,才继续去执行修改button文字的工作。
上面的结论是我们根据推理得出来的结果,但实际上里面却包含了很多渲染主线程的很多机制,也由此引出了本篇文章的第二个知识点,也就是 事件循环。
事件循环
在解释事件循环之前,我觉得有必要先了解一下关于JavaScript的一些特点,以更好地理解事件循环。
异步任务是什么
Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be able to be responsive to other events while that task runs, rather than having to wait until that task has finished. Once that task has finished, your program is presented with the result.
异步编程是一种技术,它使您的程序能够启动一个可能长时间运行的任务,并且仍然能够在该任务运行时响应其他事件,而不必等到该任务完成。一旦该任务完成,您的程序就会显示结果。
我们知道,JS 是一门单线程的语言,这是因为它运行在浏览器的渲染主线程当中,而渲染主线程只有一个。它只有一个执行上下文栈。换句话说,JavaScript代码中的所有任务都是由单个线程按顺序执行的,但是渲染主线程当中却有着非常多的任务等待着他去处理,如果采取同步执行的方法,按顺序执行。那很可能会导致主线程被阻塞,页面无法得到及时更新。为了解决这种情况,浏览器采取了异步的方式。遇到异步事件,将其交给其他线程去处理,先执行后续同步代码,待异步事件完成之后再执行。
常见的异步任务如下:
- 计时完成后需要执⾏的任务 ——
setTimeout、setInterval - ⽹络通信完成后需要执⾏的任务 --
XHR、Fetch - ⽤户操作后需要执⾏的任务 --
addEventListener
什么是事件循环 ?
前面我们讲到,JS 分为同步任务和异步任务,当遇到异步任务的时候,主线程会将其交给其他线程去处理。待其完成了之后再去执行。实际上的过程却并没有那么简单。
消息队列的基本概念
A JavaScript runtime uses a message queue, which is a list of messages to be processed. Each message has an associated function that gets called to handle the message. JavaScript 运行时使用消息队列,它是要处理的消息列表。每条消息都有一个关联的函数,调用该函数来处理消息。
JavaScript事件循环中包含一个消息队列,它存储要处理的消息列表及其关联的回调函数。在提供回调函数的情况下,这些消息会排队响应这些事件(例如单击鼠标或接收对 HTTP 请求的响应)。
例如,当用户单击按钮时,浏览器会将 click 事件添加到消息队列中。当主线程空闲时,它会将该事件检索出来并将其推到调用栈中以便执行相应的处理程序
完整过程
-
在最开始的时候,渲染主线程会进入一个无限循环
-
每⼀次循环会检查消息队列中是否有任务存在。如果有,就取出第⼀个任务执⾏,执⾏完⼀个后进入下⼀次循环
-
其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是空闲状态,则会 将其唤醒以继续循环拿取任务
举个简单的例子。现在渲染主线程遇到了一个计时任务(setTimeout函数),他并不会立即去执行计时功能,而是把它放到了计时线程去计时,等到设定的时间计时结束之后,把相对应的回调函数放到消息队列的末尾当中,等待待处理的事情结束之后,执行这个回调函数。
这样做的好处在哪呢?如果让渲染主线程等待这些任务的时机到来,那毫无疑问会让主线程长期处于一个阻塞的状态,这样不就会让浏览器卡死吗?采用这种异步的方式,可以让它永远不会处于阻塞的状态。
消息队列由多个队列构成
从刚才的解释我们不难看出,消息队列里的任务是遵循先进先出的。但是我们要知道,浏览器当中的消息队列其实有多个,不同类型的任务会被分配到不同的消息队列中,因此在处理任务时,需要按照一定规则来选择优先执行哪个任务。
在查找资料的时候,发现网上基本都提到了宏任务和微任务的说法:
- 宏任务(macrotask)是指由浏览器内核执行的任务,比如
script标签的代码、事件回调函数、setTimeout和setInterval的回调函数等。 - 微任务(microtask)是指在当前任务执行完之后立即执行的任务,它是在当前任务执行完成后、下一个宏任务执行之前执行的,比如
Promise回调函数、MutationObserver回调函数等。
宏任务和微任务会被分别加入到它们各自的任务队列中,每个任务队列都有自己的执行顺序。在执行过程中,当一个宏任务执行完成后,会立即执行所有微任务队列中的任务,然后再执行下一个宏任务。
微任务中的代码执行优先级高于宏任务,即使有多个宏任务在等待执行,只有当微任务队列中的任务全部执行完成后才会执行下一个宏任务。
Per its source field, each task is defined as coming from a specific task source. For each event loop, every task source must be associated with a specific task queue.
根据其源字段,每个任务都被定义为来自特定的任务源。对于每个事件循环,每个任务源都必须与特定的任务队列相关联。
但随着浏览器慢慢变得复杂,W3C 已经舍弃了宏任务的说法,根据上面的内容,我们可以得出:
-
每个任务都有⼀个任务类型,同⼀个类型的任务必须在⼀个队列,不同类型的任务可以分属于不同的队列。 在⼀次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
-
但是每个事件循环当中都有一个微队列,微队列的任务会优先所有任务执行。
- 添加任务到微队列的主要⽅式主要是使⽤
Promise、MutationObserver -
// ⽴即把⼀个函数添加到微队列 Promise.resolve().then(函数)
- 添加任务到微队列的主要⽅式主要是使⽤
一般来讲,目前最主要的任务优先级大致为: 微队列中的任务 ——> 交互任务 ——> 延时任务
一道常见题型
刚才讲了这么多理论知识,现在我们通过一道题目来巩固一下
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");
作者:秋华
链接:`https://juejin.cn/post/6844903796754104334`
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
由于这道题目并没有涉及交互操作,所以可以把他们简单的看成延时队列和微队列来思考(按照宏任务和微任务的思路同样没有问题),过程如下
- 首先,在主线程中开始执行全局代码,遇到
console.log("script start"),输出"script start"。 - 接着,解析器遇到
setTimeout(),执行完毕之后将其放入延时任务队列等待执行。 - 继续向下执行,遇到
async1(),立即调用该函数。函数内部的第一条语句为console.log("async1 start"),于是输出"async1 start"。 async1函数中遇到了await async2()操作,导致函数内部把剩余代码封装成一个微任务,放入微队列之中- 主线程接着执行
new Promise(),遇到console.log("promise1"),输出"promise1"。 - 由于该
Promise构造函数中传入的回调函数是同步执行的,因此该Promise实例化后立即进入resolved状态,并产生一个微任务promise2,添加到微任务队列当中 - 执行同步代码,并输出
script end - 同步代码执行完毕,从事件循环中取出微任务执行,依次输出
async1 endpromise2。执行延时队列当中的内容,输出setTimeout
结果如下:
总结
-
浏览器是一个多进程多线程的应用程序,包含多个进程和线程协同工作,以实现各种功能。
- 进程代表着不同的工厂车间,负责不同的功能;线程代表着工人,协同完成某一任务。进程之间相互独立,内存空间是独立的;线程之间可以共享相同的内存空间,相互通信。
-
渲染主线程是浏览器渲染进程中负责处理用户输入、执行JavaScript 脚本、构建DOM 和CSSOM树、布局和绘制界面等任务的核心线程。
- 在执行这些任务时,需要注意线程调度和任务优先级,以保证浏览器的稳定性和性能。
-
事件循环是
JavaScript运行时的一种机制,它允许JavaScript代码在单线程执行时仍然能够处理异步任务,它是浏览器渲染主线程的⼯作⽅式,过去把消息队列简单分为宏队列和微队列,这种说法⽬前已⽆法满⾜复杂的浏览器环境。现如今不同任务有属于他们类型的队列,有不同的优先级。但微队列有最高的优先级。
参考资料
www.ruanyifeng.com/blog/2013/0…
developer.chrome.com/blog/inside…
html.spec.whatwg.org/multipage/w…
developer.mozilla.org/zh-CN/docs/…