浏览器中的事件循环

浏览器中的多进程与多线程

浏览器是多进程的,主要包含主进程和多个渲染进程。主进程主要负责对其他进程的管理,包括创建和销毁,以及将渲染进程返回的位于内存中的Bitmap渲染到显示器上,同时负责网络资源的下载等等。而每个渲染进程则对应于一个标签页,负责管理当前标签页打开的页面DOM结构解析,JavaScript脚本执行等。

对于一个渲染进程来说,里面会有多个线程,包括GUI渲染线程、JavaScript引擎线程、事件触发线程、定时触发器线程、http异步请求线程等。前两者是保持互斥,这是为了保证DOM渲染不发生冲突,这也是为什么JavaScript是单线程的原因。浏览器是有一定刷新频率的,在一个刷新周期内,渲染进程会分别给GUI渲染线程和JS引擎线程分配时间片,前者用来刷新页面,后者则是执行当前执行队列中的任务。如果JS引擎执行时间过长,会导致页面卡顿。对于单线程的JavaScript而言,如果所有的操作都是同步的,毫无疑问会造成浏览器的阻塞,对于及时响应处理用户点击等操作是不利的。因此在浏览器环境中需要引入异步的处理机制,也就是事件循环。

浏览器中的事件循环

在讲浏览器中的事件循环之前,有必要先提一下JavaScript中的执行栈,执行栈可以理解为JS引擎当前正在执行的任务,只有将当前任务执行完,才会去检查当前事件队列中是否有任务,如果有,则压入执行栈中执行;否则继续检查事件队列中是否有新任务到来,以此循环往复,构成事件循环。一个典型的例子:

setTimeout(() => {
	console.log('async')
}, 0)
let sum = 0
for (let i = 0; i < 999999; i++) {
	sum++
}
console.log('end')
复制代码

JS引擎在执行到setTimeout函数,因为延迟时间为0,由定时触发器线程立即将回调函数放入到事件队列中,同时JS引擎继续向下执行当前执行栈中的任务,也就是一直到循环结束,打印出end之后,才会去检查事件队列中是否有任务,然后才将前面的定时器回调压入执行栈中执行。

js任务

JS引擎执行的任务可以分为两种,一种Macro Task,另一种叫Micro Task。当前正在执行的任务可能会衍生出新的Macro Task或者Micro Task,然后会被放入事件队列中,等待JS引擎当前执行栈执行完成之后,再被放入到执行栈中执行。而衍生出来的所有的Micro Task会在执行下一个Macro Task之前被放入执行栈执行,也就是说在将一个Macro Task放入执行栈之前会将当前的Micro Task队列清空。那么具体都有哪些Macro Task和Micro Task呢?

属于Macro Task的有setTimeout函数的回调、DOM事件处理函数。

属于Micro Task的有Promise对象的resolve或reject回调、MutationObserver对象的回调。

举例:

<div class="outer">
  <div class="inner"></div>
</div>
复制代码
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
复制代码

在上面的例子中,当点击inner元素时,会将inner元素的点击事件处理函数放入到Macro Task事件队列中,同时点击事件冒泡到父元素,进而触发父元素的点击事件,将同样的处理函数又放入到Macro Task事件队列中一次。此时JS引擎是空闲的,因此会从Macro Task事件队列中取出一个任务,也就是第一次放入的事件处理函数会被压入执行栈中,在执行该处理函数时会再将setTimeout函数回调放入到Macro Task事件队列中,接着又向Micro Task队列压入Promise对象的resolve回调和MutationObserver监听到元素属性发生变化的回调。

至此第一个Macro Task执行完毕,此时在取出第二个Macro Task也就是第二次被放入到Macro Task的父元素点击事件处理函数之前,JS引擎会清空Micro Task队列中所有的Task。也就是说,此时会打印出promise和mutate。然后取出第二次被放入到Macro Task的父元素点击事件处理函数并压入执行栈,同样会产生新的setTimeout Macro Task,接着清空Micro Task队列,同样打印出promise和mutate。最后剩下的两个setTimeout Macro Task先后被压入执行栈,打印了两次timeout。因此最终的打印结果:

click
promise
mutate
click
promise
mutate
timeout
timeout
复制代码

总结

文章中我们主要探讨了浏览器中和我们日常开发息息相关的渲染引擎和JS引擎线程,由JavaScript单线程的特性我们引出了解决执行同步代码阻塞的解决方案,就是事件循环。正是事件循环的作用,JS引擎线程才得以有序、非阻塞地处理各种各样的任务,包括Macro Task和Micro Task。最后,我们通过一个简单的例子,了解到Macro Task和Micro Task之间的区别以及两者的处理机制,或多或少我们也对浏览器中JS引擎工作方式的了解更进一步。文中如有不妥之处,恳请斧正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值