前端大师课

Last updated on a year ago

事件(消息)循环

浏览器进程模型

何为进程

程序运行需要自己的专属内存空间,可以把这块内存空间简单的理解为进程。每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。

何为线程

有了进程后,就可以运行程序的代码,运行代码的【人】成为【线程】,一个进程至少有一个线程,随进程开启自动创建,称为主线程,如果需要同时执行多块代码,主线程就会启动更多线程,所以一个进程可以有多个线程。

举个例子做饭,我去买菜,我妈去买肉,我爸去买调味品,就是同时进行的三个线程。

线程之间可以相互通信。

浏览器有哪些进程的线程

主要进程有:

  1. 浏览器进程:

    主要负责界面显示(不是网页的展示,是标题栏、返回按钮、刷新按钮、导航栏等)、用户交互、子进程管理。浏览器内部会启动多个线程处理不同的任务。

  2. 网络进程:

    网络通信,网络进程内部会启动多个线程处理不同的网络任务。

  3. 渲染进程:

    负责执行HTML、CSS、JS代码,默认情况下,每开启一个标签页都会开启一个新的渲染进程,以保证网页互不影响。

渲染主线程是如何工作的?

  • 解析HTML
  • 解析CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画60次
  • 执行全局JS代码
  • 执行事件处理函数
  • 执行计时器的回调函数

思考:主线程要处理这么多任务,要怎么调度任务?渲染进程为什么不适用多个线程来处理?

比如:

  • 执行js函数的时候,用户点击了按钮,应该立即执行点击事件的处理函数吗?
  • 执行js函数的时候,某个计时器到时间了,应该立即去执行它的回调吗?
  • 用户按下按钮同时计时器也到达了时间,应该处理哪一个?

主线程用排队解决这些问题。

将任务放在消息队列(message queue)中排队,一次只处理一个任务,而处理任务的过程中可能又会出现新的任务,新的任务依然放如消息队列中排队,且任务来源可以是其他线程。

具体工作步骤如下:

  1. 在最开始的时渲染主进程进入无限循环
  2. 每一次循环检查消息队列中有无任务,有则取出第一个任务,执行玩后进入下一次循环;如果没有,则进入休眠状态
  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会添加到尾部,添加新任务时,如果主线程是休眠状态,则会将其唤醒以循环拿去任务

整个过程就是事件(消息)循环

异步

在代码执行中,会遇到无法立即执行的任务,比如:

  • 记时完成后执行的任务:setTimeoutsetInterval
  • 网络通信完成后执行的任务:XHRFetch
  • 用户操作后需要执行的任务:addEventListener

如果让渲染主线程等待,就会导致主线程长期处于阻塞状态,导致卡死。所以浏览器选择异步处理

当渲染主线程收到计时任务时,会发给操作线程进行计时,操作线程计时完成后回调到渲染主线程;如果计时这段时间渲染主线程跟着等待(阻塞),则称为同步;而异步则是不等待,在渲染主线程发送计时任务后,继续从消息队列获取新任务,而当操作线程计时结束后,操作线程也不会把回调放入渲染主线程中,而是放入消息队列中。

如何理解js中的异步?

参考答案:
JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的未尾排队,等待主线程调度执行。在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<h1>Mr,Yuan is awesome !</h1>
<button>change</buttons>
<script>
var h1 = document .querySelector('h1');
var btn = document.querySelector('button')
// 死循环指定的时间
function delay(duration) {
var start = Date.now() ;
while (Date.now() - start < duration){}
}
btn.onclick = function () {
h1.textContent = "衰老师很帅!";
delay(3000);
}
</script>

按照字面理解应该是点击按钮,h1文本立刻被替换,然后再执行3000ms的计时,但是在实际情况中点击按钮后,文本不会立即被改变,而是在等待3秒后,文本才改变。

实际呢?代码上也确实是先执行替换文本,然后再进入计时。替换文本后,为什么页面不会改变呢?这是因为页面还没有渲染。而渲染也是一个任务,所以在完成文本替换之后,会在消息队列里添加渲染任务。然后继续执行死循环计时任务。等到计时任务完成之后再去执行渲染任务。

优先级

任务有优先级吗?没有,但是队列有优先级,在以前,消息队列可分为宏队列微队列,微队列的优先级更高。而在最新的W3C中,没有了宏队列,而是加入了更多的队列分类,常用的有:

  • 延时队列:用于存放计时器到达后的回调任务,优先级【中】
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级【高】
  • 微队列:用户存放需要最快执行的任务,优先级【最高】

同类型的任务必须放在同一个队列

添加任务到微队列的方式主要是使用promise,MutationObserver。如:

1
2
//立即把一个函数添加到微队列
Promise.resolve().then(函数)

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
//死循环指定时间
function delay(duration) {
var start = Data.now();
while (Data.now() - start < duration) {}
}
//设置0s的计时器
setTimeout(function() {
console.log(1);
},0)

delay(1000);

console.log(2);

执行结果是:一秒后输出2,1,渲染主线程会先执行全局的js,这个js的执行步骤如下:

  1. 执行settimout,向操作线程发送一个计时任务,由于是0秒计时,所以操作线程立即返回任务到延时队列
  2. 执行delay(1000),等待1s
  3. 执行console.log(2),控制台输出2,到此,全局js执行完毕,进入事件循环
  4. 进入事件循环,先从微队列寻找任务,没有;再从交互队列寻找,没有;然后从延时队列寻找,找到了计时器回调函数任务,执行它,控制台输出1
  5. 结果: 等待1秒后,控制台输出2 1

浏览器渲染原理

浏览器是如何渲染页面的?

当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并传递给渲染主线程消息队列,事件循环机制的作用下,渲染主线程取出队列中的渲染任务,开启渲染流程

整个渲染流程分为多个阶段:HTML解析、样式计算、布局、分层、绘制、分块、光栅化、画

解析HTML

将HTML字符串解析之后会得到两颗树:DOM树CSSOM树