浏览器事件机制
什么是事件
事件是用户操作页面时发生的交互动作,比如click/move等,事件除了用户触发的动作外,还可以是文档加载、窗口滚动和大小调整。事件被封装为一个event对象,包含了事件的属性和方法。
事件流
事件流就是事件的流向,先捕获,再到事件源,最后再冒泡,在DOM2级事件模型中,事件流分为三个阶段:事件捕获、事件处理、事件冒泡。
事件模型
- DOM0级事件模型:可以在网页中直接定义监听函数,也可以通过js来指定监听函数,直接在DOM对象上注册事件。
- IE事件模型:该事件模型共有两个过程:事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。
- DOM2级事件模型:一共三个过程:事件捕获阶段:事件从document一直向下传播到目标元素,依次检查经过的结点是否绑定了事件监听函数,如果绑定则执行。后两个阶段与IE事件模型相同。
事件冒泡
事件冒泡就是元素自身的事件被触发后,如果父元素有相同的事件,如onclick 事件,那么元素本身的触发状态就会传递,也就是冒泡到父元素,父元素的相同事件也会一级一级根据嵌套关系向外触发,直到 document/window,冒泡过程结束。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L1LWvCK4-1652435220241)(https://user-images.githubusercontent.com/70066311/168251514-1e11c64f-668e-4909-8ec1-ea866fb5a71c.png)]
如何阻止事件冒泡
- 普通浏览器:event.stopPropagation()
- IE浏览器:event.cancelBubble = true
事件委托
事件委托就是把子元素响应事件的函数委托到父元素,由父元素统一处理多个子元素的事件。事件委托的优点是内存消耗少,效率较高、可以动态绑定事件。
事件委托的局限性:对于一些事件没有事件冒泡机制,无法实现事件委托。
事件委托的使用场景
场景一:给页面的所有的a标签添加click事件,代码如下:
document.addEventListener("click", function(e) {
if (e.target.nodeName == "A")
console.log("a");
}, false);
但是这些a标签可能包含一些像span、img等元素,如果点击到了这些a标签中的元素,就不会触发click事件,因为事件绑定上在a标签元素上,而触发这些内部的元素时,e.target指向的是触发click事件的元素(span、img等其他元素)。
这种情况下就可以使用事件委托来处理,将事件绑定在a标签的内部元素上,当点击它的时候,就会逐级向上查找,直到找到a标签为止,代码如下:
document.addEventListener("click", function(e) {
var node = e.target;
while (node.parentNode.nodeName != "BODY") {
if (node.nodeName == "A") {
console.log("a");
break;
}
node = node.parentNode;
}
}, false);
场景二:动态绑定事件。在很多情况下我们会进行动态的删除或添加元素,需要给这些元素解绑或绑定事件。如果有了事件委托,那么事件绑定到父层,那么子元素的添加或删除就不会涉及到动态添加或删除事件,就可以减少很多工作。
局限性:事件委托也有局限性,例如:focus、blur之类的事件没有事件冒泡机制;mousemove、mouseout 这样的事件需要进行定位,对性能的消耗比较高,不适合事件委托。
缺点:事件委托会影响页面性能:
- 对于最底层元素,如果点击了最底层元素,那么到绑定元素之间的
DOM层数
如果太多就会影响性能。 - 对于元素,绑定事件委托的次数太多也会影响性能。
event.target 和 event.currentTarget 的区别
我们为一个元素绑定一个点击事件的时候,可以指定是要在捕获阶段绑定或者换在冒泡阶段绑定。 当addEventListener的第三个参数为true的时候,代表是在捕获阶段绑定,当第三个参数为false或者为空的时候,代表在冒泡阶段绑定。
event.target指向引起触发事件的元素,而event.currentTarget则是事件绑定的元素,只有被点击的那个目标元素的event.target才会等于event.currentTarget。
结合下面的例子,就可以很好来理解event.target和event.currentTarget:
<div id="a">
<div id="b">
<div id="c">
<div id="d"></div>
</div>
</div>
</div>
<script>
document.getElementById('a').addEventListener('click', function(e) {
console.log('target:' + e.target.id + '¤tTarget:' + e.currentTarget.id);
});
document.getElementById('b').addEventListener('click', function(e) {
console.log('target:' + e.target.id + '¤tTarget:' + e.currentTarget.id);
});
document.getElementById('c').addEventListener('click', function(e) {
console.log('target:' + e.target.id + '¤tTarget:' + e.currentTarget.id);
});
document.getElementById('d').addEventListener('click', function(e) {
console.log('target:' + e.target.id + '¤tTarget:' + e.currentTarget.id);
});
</script>
同步和异步的区别
- 同步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等待下去,直到消息返回为止再继续向下执行。
- 异步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,这个时候进程会继续往下执行,不会阻塞等待消息的返回,当消息返回时系统再通知进程进行处理。
事件循环
JS是单线程运行的,在执行代码时,JS通过将不同函数的执行上下文压入栈中保证代码的有序执行。当执行到异步任务时,JS并不会等待异步任务执行完,而是将这个事件挂起,继续执行后面的任务,当异步任务执行完后,将它的回调函数加入到任务队列中等待执行。任务队列分为宏任务队列和微任务队列,当当前的执行栈执行结束后,JS引擎会判断微任务队列中是否有任务,如果有则执行任务,然后再执行宏任务队列中的任务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U1k8hpMf-1652435220242)(https://user-images.githubusercontent.com/70066311/164969442-d06118b2-b17b-439f-b4fb-0990126c58f7.png)]
EventLoop的执行顺序如下:
- 首先执行同步代码,这属于宏任务
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
- 执行所有微任务
- 当执行完所有微任务后,如有必要会渲染页面
- 然后开始下一轮 Event Loop,执行宏任务中的异步代码
宏任务和微任务有哪些
- 微任务:promise、
- 宏任务:setTimeout、setInterval、I/O 操作、UI 渲染
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5DVUNl3T-1652435220242)(https://user-images.githubusercontent.com/70066311/168248924-b90e2dd9-f8d4-41ef-81a8-71bc10b4ada1.png)]