DOM事件小例子 ,巩固事件循环

回顾事件循环

在上节文章中通过几个小案例加强了对宏任务、微任务的理解,本文也用同样的方法举例加深对事件循环的认识。

从上图可知,在事件循环之初会先判断宏任务队列是否为空,若是空的就开始判断微任务队列是否为空,若不是空的就执行一项宏任务,接着再执行微任务直至微任务队列为空。待微任务队列清空后由浏览器判断是否需要重新渲染页面,若是则开始UI渲染,若不是则再次进入宏任务队列,执行剩余的宏任务。

案例一:DOM 事件在事件循环中的执行顺序

来看代码,这里有两个 div 分别给他们添加点击事件,使他们同步的输出 clicksetTimeout 产生一个宏任务,Promise.resolve().then 产生一个微任务,用 Mutationobserver 观察outerBtn 的属性变化,这里又会产生一个微任务。

html 复制代码
<body>
  <div class="outer-btn">外部按钮<div class="inner-btn">内部按钮</div></div>
  <script>
    var outerBtn = document.querySelector(".outer-btn");
    var innerBtn = document.querySelector(".inner-btn");

    new MutationObserver(function() {
      console.log("mutate 微任务")
    }).observe(outerBtn,{
      //观察属性变化
      attributes:true,
    });

    //点击方法
    function onClick() {
      console.log("click");

      setTimeout(function() {
        console.log("timeout 宏任务");
      });

      Promise.resolve().then(function() {
        console.log("promise 微任务");
      })

      outerBtn.setAttribute("data-number",Math.random());
    }

    outerBtn.addEventListener("click", onClick);
    innerBtn.addEventListener("click", onClick);
  </script>
</body>

注释第 29 行代码,单个按钮点击后代码运行结果如下

上述完整代码,点击内部按钮所在的div,手动触发click事件后代码运行结果如下

过程分析

  1. 当点击 div 时,宏任务队列 = onClick(inner) + onClick(outer) 。监听函数addEventListener第三个参数默认 false ,表示在事件冒泡阶段调用 ,所以先被触发的是 innerBtn的点击事件,onClick(inner) 先进入宏任务队列。
  2. 开始事件循环,执行第一个 onClick(inner),同步输出 "click"
  • 此时宏任务队列 = onClick(outer) + setTimeout
  • 此时微任务队列 = promise + mutate
  1. 执行微任务输出 promise 微任务mutate 微任务
  2. 开始执行第二个事件循环,执行第二个 onClick(outer),同步输出 "click"
  • 此时宏任务队列 = setTimeout + setTimeout
  • 此时微任务队列 = promise + mutate
  1. 执行微任务输出 promise 微任务mutate 微任务
  2. 执行第三个事件循环,输出 timeout 宏任务
  3. 执行第四个事件循环,输出timeout 宏任务,清空宏任务队列。

案例二:js点击事件对Dom事件影响

前面的案例执行顺序大家分析正确🙆了吗?

接下来的案例在之前的基础上再多添加行 js 代码,其他保持不变,看看这次你能拿下吗?

js 复制代码
<body>
  <div class="outer-btn">外部按钮<div class="inner-btn">内部按钮</div></div>
  <script>
    var outerBtn = document.querySelector(".outer-btn");
    var innerBtn = document.querySelector(".inner-btn");

    new MutationObserver(function() {
      console.log("mutate 微任务")
    }).observe(outerBtn,{
      //观察属性变化
      attributes:true,
    });

    //点击方法
    function onClick(ev) {
      console.log("click",ev.currentTarget.className);

      setTimeout(function() {
        console.log("timeout 宏任务");
      });

      Promise.resolve().then(function() {
        console.log("promise 微任务");
      })

      outerBtn.setAttribute("data-number",Math.random());
    }

    outerBtn.addEventListener("click", onClick);
    innerBtn.addEventListener("click", onClick);
    //修改为JS调用触发
    innerBtn.click() ;
  </script>
</body>

运行结果如右侧图所示,左侧图是没有JS调用的运行结果

仅仅行JS调用语句,使得两次运行结果以及顺序大不一样,这是怎么回事呢?

过程分析

在具体分析之前,我们要了解到使用js代码或者 dispatchEvent 触发的click事件不会进入宏任务队列,会作为同步代码执行。执行的顺序依旧是 onClick(inner) 再到 onClick(outer)。

  1. 执行 onClick(inner),输出 click inner-btn
  • 此时宏任务队列 = setTimeout
  • 此时微任务队列 = promise + mutate
  1. 这个时候不能紧接着清空微任务队列,因为还有 onClick(outer) 作为同步任务等待执行,于是输出click outer-btn

(在一个事件循环中,MutationObserver只产生一个微任务回调)

  • 此时宏任务队列 = setTimeout + setTimeout
  • 此时微任务队列 = promise + mutate + promise
  1. 清空微任务队列,输出 promise 微任务mutate 微任务promise 微任务
  • 此时宏任务队列 = setTimeout + setTimeout
  1. 进入第二个事件循环,输出 timeout 宏任务
  • 此时宏任务队列 = setTimeout
  1. 进入第三个事件循环,输出 timeout 宏任务,清空宏任务队列。

案例二重点总结

  1. 使用js代码或者 dispatchEvent 触发的click事件等同于执行同步代码;
  2. MutationObserver 在一个事件循环中只会产生一个微任务;

案例三:宏任务和微任务

在第三个例子中主要是想证明两方个知识点:

  1. 第一,假如主JS引擎线程一直在繁忙的工作(被同步任务阻塞)并不影响点击事件的注册;
  2. 第二,不管是宏任务还是微任务都会阻塞JS线程;
js 复制代码
<body>
  <div>
    <button id="btnStart">触发按钮</button>
  </div>
  <div style="margin-top: 10px;">
    <button class="first-btn">首次点击</button>
    <button class="second-btn">二次点击</button>
  </div>
  <script>
    var firstBtn = document.querySelector(".first-btn");
    var secondBtn = document.querySelector(".second-btn");
    //同步耗时操作
    function asyncSleep(duration) {
      const now = Date.now();
      while(now + duration > Date.now()) {

      }
    }

    firstBtn.onclick = function() {
      console.log("firstBtn onClick", new Date().toLocaleTimeString());
      //2、假设需要执行3s
      console.time("firstBtn:cost");
      asyncSleep(3000);
      Promise.resolve().then(()=>{
        console.log("执行 微任务 promise", new Date().toLocaleTimeString());
        //3、假设需要执行2s
        console.time("promise:cost");
        asyncSleep(2000);
        console.timeEnd("promise:cost")
      })
      console.timeEnd("firstBtn:cost")
    }

    secondBtn.onclick = function() {
      console.log("secondBtn onClick", new Date().toLocaleTimeString());
      //4、假设需要执行1
      console.time("secondBtn:cost");
      asyncSleep(1000);
      console.timeEnd( "secondBtn:cost" )
    }

    btnStart.onclick = function() {
      //1、假设需妥执行5s
      console.log("main:", new Date().toLocaleTimeString())
      console.time("main:cost")
      asyncSleep(5000);
      console.timeEnd("main:cost")
    }
  </script>
</body>

运行结果

过程分析

依次快速点击页面中的三个按钮,只有第一个按钮在点击时闪烁了一下,其他两个按钮没有反应,但是这并不影响他们的点击事件注册。

不管是宏任务还是微任务,都会阻塞JS线程按照队列顺序依次执行

案例四:同步变异步

设想这样一个场景,假如在代码中直接批量渲染大量的数据,那么页面在加载中势必会卡顿留白,阻塞js事件的执行,给用户造成不好的使用体验,这样的问题应该怎么优化呢?

先看问题代码:

js 复制代码
<body>
  <button type="button" class="log-btn">操作点击事件</button>
  <button type="button" class="start">开始添加dom</button>
  <script>
    var btnLog = document.querySelector(".log-btn");
    var startBtn = document.querySelector(".start");

    var array = [];
    for(var i = 1; i <= 300000; i++) {
      array.push(1); //制造300000条数据
    };
    console.log("数据制造完成");
    //渲染数据
    var renderDomList = function (data) {
      for(var i = 0, l = data.length; i < l; i++) {
        var div = document.createElement('div');
        div.innerHTML = `列表${i}`
        document.body.appendChild(div);
      }
    };

    startBtn.onclick = function() {
      console. log ("startBtn clicked:", new Date().toLocaleTimeString());
      renderDomList(array)
    }

    btnLog.onclick = function() {
      console. log ("btnLog clicked:", new Date().toLocaleTimeString());
    }
  </script>
</body>

btnLog 的回调被 startBtn的回调阻塞,只有等到页面渲染完毕后才执行。

运行结果

优化方式一:setTimeout

使用时间分片,简单点理解就是把数据拆成很多份,分批地来进行渲染。

js 复制代码
<body>
  <button type="button" class="log-btn">操作点击事件</button>
  <button type="button" class="start">开始添加dom</button>
  <script>
    var btnLog = document.querySelector(".log-btn");
    var startBtn = document.querySelector(".start");

    var array = [];
    var step = 1;
    for(var i = 1; i <= 300000; i++) {
      array.push(1); //制造300000条数据
    };
    console.log("数据制造完成");
    //渲染数据
    var renderDomList = function (data, startIndex, endIndex) {
      if(startIndex <endIndex && endIndex <= data.length) {
        setTimeout(()=>{
          for(let i = startIndex; i < endIndex; i++) {
            var div = document.createElement('div');
            div.innerHTML = `列表${i}`;
            document.body.appendChild(div);
          }
          let nextIndex = endIndex + step > data.length ? data.length : endIndex + step;
          let nextStartIndex = endIndex > data.length ? data.length : endIndex;
          renderDomList(data, nextStartIndex, nextIndex);
        },0)
      }
    };

    startBtn.onclick = function() {
      console. log ("startBtn clicked:", new Date().toLocaleTimeString());
      renderDomList(array,0,0 + step)
    }

    btnLog.onclick = function() {
      console. log ("btnLog clicked:", new Date().toLocaleTimeString());
    }
  </script>
</body>

优化后的代码采用分段式异步渲染,JavaScript 引擎不会阻塞异步任务的执行。

运行结果

此时可以看到,btnLog按钮和 startBtn按钮的回调事件执行的时间差主要由用户手动点击的时间差造成。通过把同步代码变为异步代码在一定程度上解决了阻塞问题。

优化方式二:虚拟列表

在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

优化方式三:懒加载

只展示视图上可见的数据,当滚动到页面底部时,加载更多数据,通过监听父级元素的 scroll 事件可实现。

关于后两种优化办法,网上都有很多案例,大家可以自行搜索查阅。

总结

  • 浏览器通常每秒渲染60次界面(页面平滑流畅标准),平均16ms一次渲染,由于一次事件循环的执行顺序是:一个宏任务 > 所有附属微任务 > UI渲染,所以我们要保障流畅,就必须单个宏任务和所有 附属微任务执行时间为16ms之内

  • JS调用的.click()事件、.dispathEvent()会导致事件同步调度

相关推荐
OEC小胖胖7 分钟前
js中正则表达式中【exec】用法深度解读
开发语言·前端·javascript·正则表达式·web
有一个好名字19 分钟前
vue3路由
前端·vue.js
马卫斌 前端工程师35 分钟前
npm 源切换以及添加 使用工具 nrm 使用方法
前端·npm·node.js
小彭努力中40 分钟前
49. 建模软件绘制3D场景(Blender)
前端·3d·blender·webgl
任风雨1 小时前
JWT的基础与使用
前端·网络·安全
汪子熙2 小时前
CSS Style position: absolute 的含义
前端·css
算法与编程之美2 小时前
通过两个类计算一个长方形的周长和面积
java·开发语言·javascript·jvm·servlet
晴天蜗牛不一般2 小时前
隆重的给大家介绍一下 <BaseEcharts/>
前端·npm·echarts
ceek@2 小时前
HTML增加复制模块(使用户快速复制内容到剪贴板)
前端·javascript·html
小于负无穷2 小时前
前端面试题(十)
前端