剖析setTimeout和click点击事件的触发顺序

2019-10-12 admin

下面是一段非常简单的JavaScript代码

    <div>
        <button onclick="test()">dianji</button>
    </div>
    <script>
        setTimeout(function () {
            alert('timer handler')
        }, 2000)
        function test () {
            document.addEventListener('click', function (e) {
                alert('click handler')
            }, false)

            var startTime = new Date()
            while ((new Date()).getTime() - startTime < 5000){}
        }

    </script>

但是当你点击这个按钮时,产生的效果可能会让你有些困惑。下面我们来看下:

页面打开2s内点击一次按钮

这段JavaScript代码当你在页面打开2s时间内点击一次按钮,效果是这样的:

  1. 页面卡住大约5s钟
  2. 大约5s后弹出 click handler
  3. 点击弹窗确认后,弹出 timer handler

当你继续再次点击页面中的按钮,此时依次发生:

  1. 页面卡住大约5s钟
  2. 大约5s后弹出 click handler 一次!
  3. 点击确认后又弹出 click handler 一次。

如果继续点击button按钮,会出现同样的效果,且 click handler 弹出的次数会依次增加

分析

分析这段代码来看,点击按钮后,代码执行会进入 test 函数, test函数中首先对 document 对象绑定上了一个 click 事件。然后执行了一个5s的死循环。

此时页面卡住就是因为这个死循环

ar startTime = new Date()
while ((new Date()).getTime() - startTime < 5000){}

这个死循环会导致js阻塞在这里. 在这5s时间内,2s的定时器其实在第2秒的时候已经定时完成,并把这个完成的事件放入到了任务队列中;而你在2秒之前点击的按钮这个click事件也被浏览器放入一个dom事件队列等待执行。

  • 当5s死循环的时间过去,js引擎开始变成空闲,此时点击按钮触发的这个test处理器执行完毕,js引擎便从事件队列中取出 click 事件进行执行,当前元素没有订阅click那么就冒泡到订阅了该事件的document进行执行。(会继续冒泡到document。(本质上冒泡其实是: 浏览器取出dom事件中的click事件,然后从target元素开始往上找 看下是否整个网页中还有元素订阅了这个click事件)

    由于在刚刚test函数执行期间,document对象上绑定上了 click 的监听,所以此时冒泡上来的 click 会触发document对象上的 click事件处理器, 因此弹出了 click handler.

  • 当这个 click 冒泡完毕,所有的订阅者订阅的处理器都被完全处理完,js线程再次空闲,此时去查看任务队列中的任务,发现有个2s定时器的任务已经执行完毕,js开始执行定时器的回调函数,所以弹出了 time handler

  • 当你第二次点击按钮,再次触发了 test 函数。 此时test函数内还是做了同样的事情,但是之前document上已经绑定了一个click的handler函数,所以第二次执行 test函数,会让 document对象的click处理器变成2个。 因此第二次点击按钮 click handler 会弹出2次

页面打开2s内点击一次按钮,然后第3s时点击页面空白处2次

这样操作的效果是这样的:

  1. 页面卡住大约5s钟
  2. 大约5s后弹出 ‘click handler’
  3. 弹出 ‘click handler’ 第二次
  4. 弹出 ‘click handler’ 第三次
  5. 弹出 ‘time handler’

分析

第2点之所以出现在第5点之前,在上文我们已经讲过原因了—总之,基本上是因为click触发的时刻确实就比timer触发的早,肯定要等click的handler都处理完再执行timer处理器。

但至于第3、4点为什么出现在5之前呢?这个跟2出现在5之前的原因就不一样了,因为用户在页面上的第二次和第三次点击是在2s钟之后了,此时timer定时器肯定已经完成了,但是触发 click handler 依然在 timer handler 之前。 这是为什么呢? 这主要是因为js获取任务来执行时, 点击事件的任务队列 要优先于 timer事件的任务队列。 具体可参考我的另外一篇文章 浏览器的单线程机制和事件循环

在页面卡住的5s时间内,用户在页面上点击的2次事件会放入比timer更优先的一个macroTask任务队列。由于js空闲时优先要把click事件这种更优先的macroTask任务执行完,直到任务队列为空。所以就出现了上面 click handler 要比 timer handler 更早弹出的效果。

心得

  1. js中事件可以注册多个handler形成handlers. handlers类似于一个处理器的数组。事件触发后,该事件的handler处理器会被依次执行. 这里举个跟上面有点区别的例子:假如在某个handler执行的过程中,又给该事件增加了新的handler,那么新增的这个handler不能立即执行。

    demo测试代码:

    <div>
        <button id="test" onclick="test()">dianji</button>
    </div>
    <script>
        function test () {
            alert('click handler 1');
            /* test函数触发的过程中,又给按钮绑定了新的handler; 但本次handlers遍历执行的过程中,不会执行新加入的这个handler */
            /* 因此,首次点击按钮, click handler2 不会弹出 */
            document.querySelector('#test').addEventListener('click', function (e) {
                alert('click handler 2')
            }, false);
        }
    
    </script>
    

    其实这里原理很简单:因为test元素对象上的事件handlers被触发执行的时候,类似于把数组拿出来遍历。你不可能把遍历数组和修改数组的逻辑同时运行。如:

    let a = [1,2,3]
    let count = 'x'
    a.forEach((item, index) => {
    console.log(item)
    a.push(count + index)
    })
    console.log(a)
    // 输出
    // 1
    // 2
    // 3
    // [ 1, 2, 3, 'x0', 'x1', 'x2' ]
    

    除非你addEventListener的时候,添加到冒泡的上层元素上。即下面讲的第三点。

  2. addEventListener 会给事件不断增加新的处理器handler

  3. 事件处理器handler在执行期间,事件还没有冒泡。此时还有机会给上层元素绑定事件处理器。

  4. 一个事件在冒泡过程中,要等所有订阅该事件的处理器都处理完毕,js才会去选择新的任务队列中的任务来执行。在事件触发后以及事件的冒泡过程中,会优先执行订阅了该冒泡事件的处理器,而不会去理会任务队列。

    这一条原理很简单,只需知道:js在执行同一个dom事件的所有回调处理器的过程是同步的,占用js线程执行的即可。

  5. 在事件循环中 microTask 优先于 marcroTask执行,且macroTask中也有不同优先级的队列,例如dom事件便高于timer。

[转载]原文链接:https://segmentfault.com/a/1190000020656784

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-77172.html

文章标题:剖析setTimeout和click点击事件的触发顺序

相关文章
js性能优化 如何更快速加载你的JavaScript页面
确保代码尽量简洁 不要什么都依赖JavaScript。不要编写重复性的脚本。要把JavaScript当作糖果工具,只是起到美化作用。别给你的网站添加大量的JavaScript代码。只有必要的时候用一下。只有确实能改善用户体验的时候用一下。 ...
2015-11-12
10个强大的纯CSS3动画案例分享
我们的网页外观主要由CSS控制,编写CSS代码可以任意改变我们的网页布局以及网页内容的样式。CSS3的出现,更是可以让网页增添了不少动画元素,让我们的网页变得更加生动有趣,并且更易于交互。本文分享了10个非常炫酷的CSS3动画案例,希望大家...
2015-11-16
v-charts | 饿了么团队开源的基于 Vue 和 ECharts 的图表工具
在使用echarts生成图表时,经常需要做繁琐的数据类型转化、修改复杂的配置项,v-charts的出现正是为了解决这个 痛点。基于Vue2.0和echarts封装的v-charts图表组件,只需要统一提供一种对前后端都友好的数据格式 设置简...
2018-05-24
JavaScript实现PC手机端和嵌入式滑动拼图验证码三种效果
PC和手机端网站滑动拼图验证码效果源码,同时包涵了弹出式Demo,使用ajax形式提交二次验证码所需的验证结果值,嵌入式Demo,使用表单形式提交二次验证所需的验证结果值,移动端手动实现弹出式Demo三种效果 首先要确认前端使用页面,比如...
2017-03-17
Vue获取DOM元素样式和样式更改示例
在 vue 中用 document 获取 dom 节点进行节点样式更改的时候有可能会出现 ‘style’ is not definde的错误,这时候可以在 mounted 里用 $refs 来获取样式,并进行更改: &lt;template...
2017-03-13
从2014年的发展来展望JS的未来将会如何
&lt;font face=&quot;寰�杞�闆呴粦, Arial, sans-serif &quot;&gt;2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
12个你未必知道的CSS小知识
虽然CSS并不是一种很复杂的技术,但就算你是一个使用CSS多年的高手,仍然会有很多CSS用法/属性/属性值你从来没使用过,甚至从来没听说过。 1.CSS的color属性并非只能用于文本显示 对于CSS的color属性,相信所有Web开发人员...
2015-11-12
ajax为什么令人惊异?ajax的优缺点
使用Ajax的最大优点,就是能在不更新整个页面的前提下维护数据。这使得Web应用程序更为迅捷地回应用户动作,并避免了在网络上发送那些没有改变的信息。 Ajax不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。就像DHT...
2015-11-12
HTML5的5个不错的开发工具推荐
HTML5规范终于在今年正式定稿,对于从事多年HTML5开发的人员来说绝对是一个重大新闻。数字天堂董事长,DCloud CEO王安也发表了文章,从开发者和用户两个角度分析了HTML对两个人群的优势。其实,关于HTML5的开发工具,我们以往的...
2015-11-12
JavaScript教程:JS中的原型
Keith Peters 几年前发表的一篇博文,关于学习没有“new”的世界,其中解释了使用原型继承代替构造函数。两者都是纯粹的原型编码。 标准方法(The Standard Way) 一直以来,我们学习的在 JavaScript 里创建对...
2015-11-12
回到顶部