文章目录
1. 浏览器的进程
首先我们要知道,浏览器是
多进程多线程
的应用程序
一个浏览器的主要进程包括 三个,分别是以下三个
- 浏览器进程
- 网络进程
- 渲染进程
其实还有其他的进程,比如:GPU 进程
、插件进程
等
具体的职责
1.1 浏览器进程
职责
- 管理浏览器窗口和标签页
- 处理用户界面(如地址栏、书签、前进/后退按钮
- 协调其他进程的工作。
注意:每个浏览器实例,只有一个
主进程
1.2 网络进程
就是处理所有的网络请求的,比如
HTTP
、HTTPS
、WebSocket
1.3 渲染进程
渲染进程启动后,会开启一个
渲染主线程
,主要负责执行html、css、js代码。
一般来说,每个标签页,会有一个独立
的渲染进程
,但目前有的为了浏览器性能优化,可能会存在同一个站点共用
一个渲染进程
2. 渲染主线程
渲染主线要处理好多东西
- 解析 HTML:将 HTML 文档解析为
DOM(Document Object Model)树
- 解析 CSS:将 CSS 文件解析为
CSSOM(CSS Object Model)
树 - 构建渲染树(Render Tree):将 DOM 树和 CSSOM 树结合,生成 渲染树
- 布局(Layout):计算渲染树中每个节点的几何信息(如位置、大小)
- 执行JS脚本
- 处理事件
- 处理网络请求
- 等等
3 事件循环
- 在最开始的时候,渲染主线程会进入一个
无限循环
- 每一次循环会检查消息队列中
是否
有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠
状态。 - 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的
末尾
。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
这样一来,就可以让每个任务有条不紊的、持续的进行下去了。
整个过程,被称之为事件循环(消息循环)
如何解释 事件循环呢?
完整点的解释就是i下面这样
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式
在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列
,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据 W3C官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列
,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列
,微队列的任务一定具有最高
的优先级,必须优先调度执行。
4. 异步
浏览器在执行我们写的代码的时候,难免会遇到一些
无法立即处理
的代码,比如
- 计时完成后需要执行的任务 ——
setTimeout
、setInterval
- 网络通信完成后需要执行的任务 –
XHR
、Fetch
- 用户操作后需要执行的任务 –
addEventListener
如果让渲染主线程一直等着这些任务执行或者到达执行时机,那渲染主线程就是长期处于
阻塞
了,就是 浏览器上就卡死
了。所以JS 引用了异步
这个概念来处理这些问题
JS 引入异步编程的原因是为了解决单线程模型的局限性,避免
阻塞主线程
,提高程序的性能
和用户体验
。通过事件循环
、回调函数
、Promise
和 Async/Await 等机制,JavaScript 能够高效地处理异步任务
这样的话,就可以让渲染主线程不阻塞(如若写个死循环还是会阻塞)
如何解释 JS 的 异步呢
js是一门单线程语言,这是因为它运行在浏览器的渲染主线程里面,而渲染主线程只有一个
,而渲染主线程承担着诸多的工作,渲染界面、执行JS 都在其中运行
如果使用同步的方式,就极有可能导致主线程产生阻塞
,从而导致消息队列中的很多其他任务无法得到执行,这样一来 一方面会导致繁忙的主线程白白的消耗时间,另一方面导致界面无法及时更新,给用户造成卡死现象
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程
去处理,自身立即结束任务的执行,转而执行后续代码
。当其他线程完成时,将事先传递的回调函数包装成任务
,加入到消息队列的末尾排队
,等待主线程调度执行
。
这种模式下浏览器用不阻塞,从而最下限度的保证了单线程的流畅运行
4.1 阻塞的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>测试按钮</h2>
<script>
let heEL = document.querySelector('h2')
heEL.addEventListener('click', function () {
heEL.innerText = '你点击了'
var strat = Date.now()
while (Date.now() - strat < 3000) { }
})
// 结果是,会先等待三秒,然后再更改文字
// 在 JavaScript 中,事件处理程序的更改会在下一次重新渲染之前应用。因为浏览器是单线程的,所以当 JavaScript 执行时,它会阻塞其他任务(例如重新渲染页面)。
// 在你的代码中,当点击事件触发时,你先将 < h2 > 元素的文本更改为 '你点击了'。然后,你进入一个 while 循环,并且该循环会一直运行 3 秒钟。由于 JavaScript 是单线程的,这个循环会阻塞其他任务(例如页面的重新渲染),因此页面上的更改将无法立即显示。
// 当循环结束后,JavaScript 引擎才能够继续执行其他任务,其中包括重新渲染页面。在重新渲染之后,你的代码才会生效,将 < h2 > 元素的文本更改为 '你点击了'。
</script>
</body>
</html>
可以看上面的GIF 就会发现,点击按钮后,是
等待了三秒后
,界面上才渲染为你点击了
,按照正常来说应该是点击后立即更新。
DOM 更新是同步了,执行到
heEL.innerText = '你点击了'
会立即更新 DOM 树。
但是界面的渲染是要等主线程空闲时才会进行,这个循环会阻塞三秒,所以界面不会立即显示更新后的内容
从这个图也可以看到,这个 绘制,这一步操作,是在点击事件执行完
以后
的
heEL.innerText = ‘你点击了’; 的具体流程
- DOM 更新:JavaScript 更新 < h2 > 元素的文本内容。
- 样式计算:浏览器重新计算 < h2> 元素的样式。
- 布局:如果文本内容的变化影响了布局,浏览器会重新计算布局。
- 绘制:浏览器将更新后的内容绘制到屏幕上。
- 合成:如果有多个图层,浏览器会将它们合并为最终的图像。
- 显示:最终图像显示在屏幕上。
5. 消息队列的优先级
任务没有优先级,在消息队列中先进先出 ,但消息队列是有优先级的
根据 W3C 的最新解释:
-
每个任务都有一个
任务类型
,同一个类型的任务必须
在一个队列,不同类型的任务可以分属于不同的队列。 -
在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
-
浏览器必须准备好
一个微队列
,微队列中的任务优先
所有其他任务执行https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
随着浏览器的复杂度急剧提升,W3C 不再使用
宏队列
的说法
在目前 chrome 的实现中,至少
包含了下面的队列:
延时队列
:用于存放计时器到达后的回调任务,优先级「中
」交互队列
:用于存放用户操作后产生的事件处理任务,优先级「高
」微队列
:用户存放需要最快执行的任务,优先级「最高
」
6. 实际练习
6.1 第一个
- 渲染主线程开始执行代码,首先会输出
1
- 发现定时器,放入
延时队列
- 输出
3
- 发现 Promise (new Promise 是
同步代码
),发现第一个then 放入微队列
,然后还有一个 then 也放入进去,等待渲染主线程先执行完毕 - 输出
6
- 这个时候主线程执行完毕,就要看 微队列,有没有东西,如若发现有就执行,这个时候就会先输出
5
,然后输出6
7. 其他
之前,事件循环,不是队列的概念,是
宏任务
和微任务
的概念
先执行
宏任务
,执行完成后,看看是否有微任务,如若有,则拿出来执行,如若都执行完成后,在开始新的宏任务,具体的宏任务和微任务有这样