前端大师课
Last updated on a year ago
事件(消息)循环
浏览器进程模型
何为进程
程序运行需要自己的专属内存空间,可以把这块内存空间简单的理解为进程。每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。
何为线程
有了进程后,就可以运行程序的代码,运行代码的【人】成为【线程】,一个进程至少有一个线程,随进程开启自动创建,称为主线程,如果需要同时执行多块代码,主线程就会启动更多线程,所以一个进程可以有多个线程。
举个例子做饭,我去买菜,我妈去买肉,我爸去买调味品,就是同时进行的三个线程。
线程之间可以相互通信。
浏览器有哪些进程的线程
主要进程有:
浏览器进程:
主要负责界面显示(不是网页的展示,是标题栏、返回按钮、刷新按钮、导航栏等)、用户交互、子进程管理。浏览器内部会启动多个线程处理不同的任务。
网络进程:
网络通信,网络进程内部会启动多个线程处理不同的网络任务。
渲染进程:
负责执行HTML、CSS、JS代码,默认情况下,每开启一个标签页都会开启一个新的渲染进程,以保证网页互不影响。
渲染主线程是如何工作的?
- 解析HTML
- 解析CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画60次
- 执行全局JS代码
- 执行事件处理函数
- 执行计时器的回调函数
- …
思考:主线程要处理这么多任务,要怎么调度任务?渲染进程为什么不适用多个线程来处理?
比如:
- 执行js函数的时候,用户点击了按钮,应该立即执行点击事件的处理函数吗?
- 执行js函数的时候,某个计时器到时间了,应该立即去执行它的回调吗?
- 用户按下按钮同时计时器也到达了时间,应该处理哪一个?
主线程用排队解决这些问题。
将任务放在消息队列(message queue)中排队,一次只处理一个任务,而处理任务的过程中可能又会出现新的任务,新的任务依然放如消息队列中排队,且任务来源可以是其他线程。
具体工作步骤如下:
- 在最开始的时渲染主进程进入无限循环
- 每一次循环检查消息队列中有无任务,有则取出第一个任务,执行玩后进入下一次循环;如果没有,则进入休眠状态
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会添加到尾部,添加新任务时,如果主线程是休眠状态,则会将其唤醒以循环拿去任务
整个过程就是事件(消息)循环
异步
在代码执行中,会遇到无法立即执行的任务,比如:
- 记时完成后执行的任务:
setTimeout
、setInterval
- 网络通信完成后执行的任务:
XHR
、Fetch
- 用户操作后需要执行的任务:
addEventListener
如果让渲染主线程等待,就会导致主线程长期处于阻塞状态,导致卡死。所以浏览器选择异步处理
当渲染主线程收到计时任务时,会发给操作线程进行计时,操作线程计时完成后回调到渲染主线程;如果计时这段时间渲染主线程跟着等待(阻塞),则称为同步;而异步则是不等待,在渲染主线程发送计时任务后,继续从消息队列获取新任务,而当操作线程计时结束后,操作线程也不会把回调放入渲染主线程中,而是放入消息队列中。
如何理解js中的异步?
参考答案:
JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的未尾排队,等待主线程调度执行。在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
小例子:
1 |
|
按照字面理解应该是点击按钮,h1文本立刻被替换,然后再执行3000ms的计时,但是在实际情况中点击按钮后,文本不会立即被改变,而是在等待3秒后,文本才改变。
实际呢?代码上也确实是先执行替换文本,然后再进入计时。替换文本后,为什么页面不会改变呢?这是因为页面还没有渲染。而渲染也是一个任务,所以在完成文本替换之后,会在消息队列里添加渲染任务。然后继续执行死循环计时任务。等到计时任务完成之后再去执行渲染任务。
优先级
任务有优先级吗?没有,但是队列有优先级,在以前,消息队列可分为宏队列和微队列,微队列的优先级更高。而在最新的W3C中,没有了宏队列,而是加入了更多的队列分类,常用的有:
- 延时队列:用于存放计时器到达后的回调任务,优先级【中】
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级【高】
- 微队列:用户存放需要最快执行的任务,优先级【最高】
同类型的任务必须放在同一个队列
添加任务到微队列的方式主要是使用promise,MutationObserver。如:
1 |
|
例子
1 |
|
执行结果是:一秒后输出2,1,渲染主线程会先执行全局的js,这个js的执行步骤如下:
- 执行settimout,向操作线程发送一个计时任务,由于是0秒计时,所以操作线程立即返回任务到延时队列中
- 执行delay(1000),等待1s
- 执行console.log(2),控制台输出2,到此,全局js执行完毕,进入事件循环
- 进入事件循环,先从微队列寻找任务,没有;再从交互队列寻找,没有;然后从延时队列寻找,找到了计时器回调函数任务,执行它,控制台输出1
- 结果: 等待1秒后,控制台输出2 1
浏览器渲染原理
浏览器是如何渲染页面的?
当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并传递给渲染主线程的消息队列,事件循环机制的作用下,渲染主线程取出队列中的渲染任务,开启渲染流程
整个渲染流程分为多个阶段:HTML解析、样式计算、布局、分层、绘制、分块、光栅化、画
解析HTML
将HTML字符串解析之后会得到两颗树:DOM树和CSSOM树