事件循环深入理解
有关事件循环的重新学习和一些新的理解
浏览器的进程模型
进程
众所周知,程序运行都需要开辟一块内存空间,分配至少一个进程,浏览器也不例外。一般来说,浏览器为了完成复杂的任务,会开辟多个进程:
- 浏览器进程 主要负责界⾯显示、⽤户交互、⼦进程管理等。浏览器进程内部会启动多个线程处理不同的任务。
- 网络进程 负责加载⽹络资源。⽹络进程内部会启动多个线程来处理不同的⽹络任务。
- 渲染进程 在渲染进程中,会优先开辟一个渲染主线程,该线程用于执行我们熟知的HTML,CSS,JS代码等。默认情况下,浏览器会为每一个tab页面开辟一个新的渲染进程。 我们接下来主要分析渲染进程中的内容。
线程
我们可以把一个进程看作是一个工厂,一个线程是一个工人,一个进程中至少会存在一个主线程,该线程结束则意味着进程也结束了, 浏览器也是基于这样的基本结构的。在渲染进程中,一般主线程为渲染主线程,该线程执行了非常多的任务,比如:
- 解析html代码
- 解析css
- 计算样式
- 计算布局
- 处理图像
- 执行全局的js代码
- 执行事件处理的函数
- 执行计时器的函数
- ...等
为了解决这些问题,浏览器安排了一个模型来完成各种任务之间的调度-排队,也就是我们常说的EventLoop,在浏览器的实现中一般被称为MessageQueue。
事件循环做了几件事:
- 类似一个java中的main方法,渲染主线程中存在一个run方法,run方法开启了一个无限循环,类似如下:
let messageQueue = [] for(;;){ //每次在这个循环中,将从各种队列中取出一个存在的任务,比如微队列、延时队列、交互队列等 //根据w3c的最新标准,浏览器必须实现的队列仅存在微队列,其它的队列一般来说由浏览器决定是否实现 }
- 当任务队列中存在一个任务时,将其按照先进先出的顺序,取出并放入到主线程执行。
- 如果当前所有的任务队列中都没有任务,则取出任务的动作将陷入沉睡,直到任务队列中新增了一个任务,从任务队列中重新唤醒该动作。
- 向任务队列中添加任务这个动作是任何时候都能够开启的,包括其它进程的线程也可以向本线程添加任务,比如浏览器进程监听的用户操作,点击,滚动等。
其它相关概念
浏览器的异步任务
浏览器在执行过程中,会遇到的一些无法立即处理的任务,浏览器无法立即执行,也无法持续等待,所以采取了异步的方式,将其回调延迟执行。
- 定时任务
setTimeout
setInterval
这类型的任务会被渲染主线程置入os模块,调用系统计时器,系统计时器完成计时后,就可以将回调函数置入到延时队列等待执行。 - 网络任务
xhr
fetch
ajax
这类型的任务通常会以promise.resolve或者promise.reject的方式返回,他们自己的线程将回调函数置入到微队列,在主线程任务完成之后优先执行该回调函数。 - 交互任务
addEventListener
通常,交互任务会被浏览器置为优先级仅次于微任务队列的队列中。
接下来看一些实际的例子,来帮助理解异步和线程之间的关系,我们从简单到难:
- 单纯的主线程任务,即初始的js代码这段代码应该是最简单的,不需要任何的解释,它输出了 2 1
function fun1() { console.log(1) } console.log(2) fun1() //out 2 1
- 主线程任务中含有延时任务上述代码,稍有编码经验的应该都没问题,执行顺序是3 1 2,虽然确实很简单,但是这里我想借助这个代码分析一下上述的线程问题: 首先我们可以确定的是,这段代码中总共需要一个延时队列,一个渲染主线程,那么分析结果就如下图:
function log(val) { console.log(val) } setTimeout(function () { log(1) }, 0); setTimeout(function () { log(2) }, 1000); log(3) //out 3 1 sleep 1s 2
- 主线程任务中有promise或者MutationObserver或者queueMicrotask
function log(val) { console.log(val) } setTimeout(function () { log(1) }, 0); setTimeout(function () { log(2) }, 1000); let promise = new Promise(resolve => { console.log('init promise') setTimeout(()=>{ resolve('promise resolve') }, 999) }) promise.then(res=>{ console.log(res) }) log(3) //out 3 1 sleep 999ms promis resolve 1ms 2
在这段代码中,我们加入了一个promise,它将一个settimeout塞到了微任务中,这个微任务又将回调函数塞到了延时队列中。如果等待时间不是999而是1000,那么执行结果将是 3 1 2 promise resolve
注意
请注意,这段代码有部分人拿到编辑器或者node环境下执行可能会有不同的结果,在处理上有些许差异!
js为什么有时候会阻塞页面渲染
在开发过程中,我总会发现有些很奇怪的代码,明明点击了一个按钮或者是改变了一个文本,但是要隔很久才出现效果,不知道其它小伙伴有没有遇到过,我们结合一段代码来说一下这种情况。
<h1>我是一段文本</h1>
<button>change</button>
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);
};
现在问题来了,当change按钮被点击后,文本会立马改变吗?
答案是文本内容实际上已经被改变了,但是视觉效果上要延迟三秒才改变。为什么会造成这种情况,引入我们上面说过的各种任务队列的知识:
- 一开始,主线程读取了js代码,定义了h1,btn,delay函数,并且向其它进程(浏览器进程)添加了一个监听。
- 随后点击事件发生,执行到
h1.textContent = '这段文本现在被改变了'
,现在文本已经被改变了。但是文本改变了,不代表页面能改变,上面提到了,主线程负责各种各样的事情, 比如执行html,css,js,绘制等等,现在内容改变了,页面是需要重新绘制的,所以主线程向一个任务队列中添加了一个重新绘制的任务,该任务和其他的队列任务一样正在排队。 - 随后执行到
delay(3000)
,在这个函数中,很容易看到,这是在堵塞主线程3000ms,所以主线程在这3s的时间里一直在执行delay函数的内容,并不认为当前函数已经结束了。 - 3s结束后,再去其它的队列中获取到重新渲染的任务,才将h1的内容改变。
任务有没有优先级
任务没有优先级,但是队列存在优先级
在w3c的标准中,微任务队列优先级最高,其它的队列w3c并没有进行强硬的要求,但是,从v8引擎的源代码来看,交互队列的优先级仅次于微队列。
好啦,本篇的内容就到这啦,下一次是浏览器的渲染原理。