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()会导致事件同步调度

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui