消息队列和事件循环
使用单线程处理安排好的任务
一开始,我们是通过把所有任务按照顺序写进主线程,线程执行时,这些任务就按照顺序在线程中依次执行
所有任务执行完成之后,线程会自动退出
function MainThread(){
let num1 = 1 + 2
let num2 = 20/5
let num3 = 7 * 8
console.log(num1, num2, num3)
}
在线程处理过程中处理新任务
目前,我们的任务都是执行之前安排好的
但是事实上,很多任务都会在执行过程中产生,并不是一开始就安排好的
所以现在想要在线程运行过程中接收新的任务,就需要采用事件循环机制
所以这一版的线程相比上一版做了改进:
- 引入了循环机制,具体实现方式在线程语句最后加上一个
for循环,线程一直循环执行 - 引入了事件,能够让用户输入信息激活线程
处理其他线程发送过来的任务
在第二版的线程中,我们所有任务都是来自于线程内部的
如果现在其他线程想让主线程执行一个任务,那么利用第二版线程模型则无法做到
在这个模型中可以将线程模型分为三部分:
- 添加一个消息队列
- IO线程中产生的新任务添加进消息队列尾部
- 渲染主线程会循环的从消息队列头部中读取任务,执行任务
这样第三版的线程模型就改造完毕:主线程执行的任务都全部从消息队列中获取,所以如果有其他线程想要发任务给主线程,只需要将任务添加到消息队列中就可以了
处理其他进程发送过来的任务
现在我们已经实现了线程之间的消息通信
那么进程之间的消息通信呢,如何处理其他进程发送过来的任务
实际上,渲染进程中有一个IO线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程
消息队列中的任务类型
这里面包含了很多内部消息类型,如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等等
除此之外,消息队列中还包含了很多与页面相关的事件,如 JavaScript 执行、解析DOM、样式计算、布局计算、CSS 动画等
这些事件都是在主线程中执行的,编写Web时,需要衡量这些事件的占用时长,并想办法解决单个任务占用主线程过久的问题
如何安全退出
要退出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志
如果设置了,则直接中断当前所有任务,退出线程
页面使用单线程的缺点
这里基于消息队列的先进先出特点,就出现了以下两个问题
-
如何处理高优先级任务
一个典型的场景是监控 DOM 节点的变化情况(节点的插入、修改、删除等动态变化),然后根据这些变化来处理相应的业务逻辑
一个通用的设计的是利用 JavaScript设计一套监听接口,当变化发生时,渲染引擎同步调用这些接口,这是一个典型的观察者模式
这个模式存在一些问题,因为DOM通常都变化的十分频繁,每次变化都调用JavaScript接口会导致该次任务执行的时间拉长,导致执行效率的降低
如果将这种改变做成异步的消息事件添加到消息队列的尾部,又会影响到监控的实时性,因为此刻可能已经由很多任务已经在排队了
为了权衡效率和实时性,出现了微任务
通常我们把消息队列中的任务称为宏任务,每个宏任务中又包含一个微任务队列。在执行宏任务的过程中,如果有DOM变化,那么就会将变化添加到微任务队列中,等宏任务中的主要功能直接完成之后,渲染引擎不着急去执行下一个宏任务,而是执行当前宏任务中的微任务队列,这样就解决了实时性问题
-
如何解决单个任务执行时长过久的问题
如果在执行动画过程中,其中有个 JavaScript 任务因执行时间过久,占用了动画单帧的时间,这样会给用户制造了卡顿的感觉,这当然是极不好的用户体验。针对这种情况,JavaScript 可以通过回调来规避这种问题,也就是让要执行的JavaScript 任务滞后执行
浏览器记录页面加载过程
使用开发者工具中的Performance标签,选择左上角的start porfiling and load page来记录整个页面加载过程的事件执行情况