下面是一段非常简单的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>
但是当你点击这个按钮时,产生的效果可能会让你有些困惑。下面我们来看下:
这段JavaScript代码当你在页面打开2s时间内点击一次按钮,效果是这样的:
当你继续再次点击页面中的按钮,此时依次发生:
如果继续点击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次
这样操作的效果是这样的:
第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 更早弹出的效果。
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的时候,添加到冒泡的上层元素上。即下面讲的第三点。
addEventListener 会给事件不断增加新的处理器handler
事件处理器handler在执行期间,事件还没有冒泡。此时还有机会给上层元素绑定事件处理器。
一个事件在冒泡过程中,要等所有订阅该事件的处理器都处理完毕,js才会去选择新的任务队列中的任务来执行。在事件触发后以及事件的冒泡过程中,会优先执行订阅了该冒泡事件的处理器,而不会去理会任务队列。
这一条原理很简单,只需知道:js在执行同一个dom事件的所有回调处理器的过程是同步的,占用js线程执行的即可。
在事件循环中 microTask 优先于 marcroTask执行,且macroTask中也有不同优先级的队列,例如dom事件便高于timer。
惊为天人的发现promise里面的一个例子:原来 setTimeout居然还有第三个参数,调用方法的时候可以作为传参对象。定时器启动时,第三个及以后的参数是作为第一个参数(也就是函数)的参数传进去的。
今天要说的很简单,没有 setTimeout 的基本用法,也没有什么特殊用法。就是想记录一下 setTimeout 的一个特殊情况,分享给可能也不知道的你们。其中第二个参数是需要延时执行的毫秒数,大家应该都知道这个时间是不准确的,可以理解为最短延时。
计时器setTimeout是我们经常会用到的,它用于在指定的毫秒数后调用函数或计算表达式。语法:setTimeout(code, millisec, args);注意:如果code为字符串,相当于执行eval()方法来执行code。
在js中setTimeout和setInterval都是用来定时的一个功能,下面这篇文章主要给介绍了JS中setInterval和setTImeout的this指向问题,文中通过示例介绍的很详细,有需要的朋友可以参考借鉴,一起来看看吧。
说起来你可能不相信,setTimeout居然有第三个参数,我以前也没用过这个,是前几天看别人博客发现的,咋一看还以为写错了吧,下面一起看看这个setTimeout第三个参数。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!