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

相关推荐
辻戋2 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保2 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun3 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp3 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.4 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl6 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫7 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友7 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理9 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻9 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js