JavaScrip的事件循环

处理事件(事件循环机制)

当事件发生时,浏览器调用相应的事件处理器,由于js是单线程执行,在用一个时刻只能处理一个事件。

在事件处理阶段,例如用户发生了移动和点击事件。会把这些事件放在事件队列里边,事件循环会检查队列,发现队列的前面有一个移动事件,移动事件处理完后,js退出处理器函数,事件循环会再次检查队列,这一次,在队列的最前面,发生了点击事件,然后处理完在退出。一旦单击处理器执行完成。队列中不在有新的事件,事件循环会继续循环等待新的事件。这个循环会一直执行到用户关闭web应用。

深入事件循环

事件循环不仅仅包含事件队列,而是具有至少两个队列,除了事件,还要保持浏览器执行的其他操作。这些操作被称为任务,并且分为两类:宏任务和微任务。

宏任务:建立主文档对象、解析HTML、执行主线(或全局)JS代码,更改当前URL以及各种事件,页面加载、输入、网络事件和定时器事件。从浏览器角度来看,宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度。

微任务:更小的任务,微任务更小应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面UI。微任务的案例包括promise回调函数、DOM发生变化等。微任务需要尽快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的重绘,UI重绘会使应用程序的状态不连续。

重绘:一些元素需要更新属性,而这些属性只是影响元素外观,风格,而不会影响布局,比如background-color。就称为重绘。

回流:因为元素的规模尺寸、布局,隐藏等改变而需要重新构建。这称为回流。

回流必将引起重绘,重绘不一定会引起回流。

JS是单线程,事件循环基于两个基本原则:

  • 一次处理一个任务
  • 一个任务后知道运行完成,不会被其他任务中断。
    在这里插入图片描述
    从全局来看,事件循环首先检查宏任务队列,如果宏任务等待,则立即执行宏任务。直到该任务运行完(或者队列为空),事件循环将去处理微任务。如果有任务在该队列中等待,则事件循环将开始依次执行,直到队列所有微任务执行完毕。处理宏任务和微任务队列的区别是:单次循环迭代中,最多处理一个宏任务(其余的在队列中等待),而队列中的微任务都会被处理。

当微任务队列处理完后,事件循环会检查UI是否需要更新渲染,如果是,则会重新渲染UI视图。至此,当前事件循环结束,回到最初的第一个环节,在检查宏任务队列,开心新一轮事件。

  • 两类任务都是独立于事件循环的,意味着任务队列的添加行为也发生在事件循环之外。
  • 因为JS基于单线程执行模式,两类任务逐个执行。当一个任务开始后,在完成前不会中断。除非浏览器终止,如某个任务执行实践过长或内存占用大。
  • 所有微任务会在下一次渲染之前执行完成,它们的目标是在渲染前更新应用装态。
<button id="firstButton"></button>
<button id="secondButton"></button>
<script>
    const firstButton = document.querySelector("#firstButton");
    const secondButton = document.querySelector("#secondButton");
    firstButton.addEventListener("click", function firstHandler() {
        /*单击运行8ms的处理代码*/
        Promise.resolve().then(() => {
            /*运行4 ms在传入回调函数*/
        }); // ⽴即对象promise,并且执⾏then⽅法中的回调函数
    });
    secondButton.addEventListener("click", function secondHandler() {
        /*单击运行5ms的处理代码*/
    });
</script>

在本例中,我们假设发生以下行为

  • 5ms单击firstButton
  • 12ms单击secondButton
  • firstButton的单击事件处理函数firstHandler需要执行8ms
  • secondButton的单击事件处理函数secondHandler需要执行5ms
  • Promise回调函数执行4ms

firstHandler函数中创建立即兑现的Promise,并需要4ms传入回调函数。因为Promise表示当前未知的一个未来值,因此Promise处理函数总是异步执行。

在本例中,因为我们已知Promise成功兑现。但是为了连续性,js引擎会在firstHandler代码执行完成后在异步调用回调函数。通过创建微任务,将回调放入微任务队列。
在这里插入图片描述
在执行主线程js后,页面重新渲染,开启新一轮的事件循环。发现宏任务队列里有firstHandler宏任务要处理,在15ms添加了一个微任务Promise成功兑现进入微任务队列,继续执行宏任务8ms后,23ms发现有微任务,处理Promise回调函数(执行4ms)。微任务队列为空。页面可以重新渲染。又开启新一轮的事件循环。处理宏任务secondHandler5ms结束。微任务队列为空。页面重新渲染。这段代码的事件循环就结束了。

事件循环中执行计时器

<button id="myButton"></button>
<script>
setTimeout(function timeoutHandler(){
/*执行回调函数需要6ms*/
}, 10); //注册10ms后延迟执⾏函数
setInterval(function intervalHandler(){
/*执行回调函数需要8ms*/
}, 10); //注册每10ms执⾏的周期函数
 const myButton = document.getElementById("myButton");
 myButton.addEventListener("click", function clickHandler(){
 /*单击运行10ms的处理代码*/
 }); //为按钮单击事件注册事件处理器
    
 /*运行18毫秒的代码*/
</script>

现在假设用户在程序执行6ms时快速单击按钮。
在这里插入图片描述
在执行过程中,发生3个重要事件:

  • 0mssetTimeoutsetInterval时间间隔是10ms,计时器的引用保存在浏览器中。
  • 6ms,单击鼠标。
  • 10mssetTimeoutsetInterval第一个时间间隔触发。

6ms单击按钮,该任务被添加到队列中,10ms时,setTimeoutsetInterval触发。也都被添加到队列中去。代码运行18ms之后,初始化代码结束。微任务中没有任务,可以重新渲染。进行下一个事件循环迭代。

18ms结束时,3个任务正在等待执行:单击事件处理器,setTimeoutsetInterval时间处理器。单击事件在队列的前方最先执行(假设需要耗时10ms)。
在这里插入图片描述
28ms单击事件处理器完成,可以从新渲染,事件循环开始下一次迭代,处理setTimeout事件(假设耗时6ms),处理完成到34ms

在这段时间内,第30mssetInterval计时器时间间隔触发。这一次不会添加新的任务到队列中去,因为队列中已经有一个与之相匹配的间隔计时器34ms时第一次添加的setInterval任务才执行(假设耗时8ms)。42ms执行完成。在40ms的时候setInterval计时器时间间隔触发,由于间隔处理器正在执行(不是在队列中等待),一个新的setInterval任务添加到任务队列中去,程序继续执行。

可以看出,事件循环一次只能处理一个任务,不能确定定时器处理程序是否会执行我们期望的确切时间。间隔处理程序尤其如此。在本例中少执行了一次回调函数。回调函数没有预期的时间10、20、30、40ms去执行。setTimeout代码也在10ms后才执行(等待时间是会大于10ms,取决于事件队列状态)。

事件循环从web应用开始时就存在,首先检查宏任务队列(建立主文档对象、解析HTML、执行主线(或全局)JS代码,更改当前URL以及各种事件,页面加载、输入、网络事件和定时器事件),执行队列第一个任务(宏任务一次迭代中只会执行一个),在检查微任务队列(promise、DOM发生变化等),把当前所有的微任务队列执行完。告诉浏览器可以从新渲染。然后又开始新的一轮事件循环。这个循环会一直执行到用户关闭web应用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值