浏览器:事件模型、移动端的点击延迟与穿透

1,415 阅读3分钟

事件模型

事件传播

  • 事件捕获 捕获阶段依次为window、document、html、body、div、button
  • 冒泡阶段 button、div、body、html、document、window

CSS对事件传播的影响

  1. 父子dom元素间事件传播不受CSS的影响,只取决于DOM树
<div id="outer" style="height:10px;background-color: red;">
    <button id="inner1" style="position: absolute;top: 20px">点击</button>
</div>

  1. 兄弟节点间的层叠关系会影响事件的触发
<div id="outer" style="height:10px;background-color: red;">
        <button id="inner1" style="position: absolute;top: 20px">点击</button>
        <button style="position: absolute;top: 20px">覆盖</button>
    </div>

解决方法是在遮罩元素的css属性加上pointer-event:none使之不能成为事件的目标节点

 <button style="position: absolute;top: 20px; pointer-events: none;">覆盖</button>

事件代理

由于事件冒泡的特性,不用在所有子节点上绑定事件监听,只用在父元素上绑定,通过事件对象的target属性可以判断事件冒泡的最底层dom元素

<body>
    <div id="outer">
        <button id="inner1">点击</button>
        <button id="inner2">点击</button>
    </div>
    <script>
        document.getElementById('outer').addEventListener('click', outterClick)
        document.getElementById('inner1').addEventListener('click', inner1Click)
        document.getElementById('inner2').addEventListener('click', inner2Click)

        function outterClick(e) {
            console.log('outter', e.target, this, e.target === this) // false
        }

        function inner1Click(e) {
            console.log('inner1', e.target, this, e.target === this) // true
        }

        function inner2Click(e) {
            console.log('inner2', e.target, this, e.target === this)
        }
    </script>
</body>

e.currentTarget应该就是this(当前的dom元素),但是测试时bull

阻止事件传播

e.stopPropagation()

<body>
    <div id="outer">
        <button id="inner1">点击</button>
    </div>
    <script>
        document.getElementById('outer').addEventListener('click', outterClickDown, true)
        document.getElementById('outer').addEventListener('click', outterClickUp)
        document.getElementById('inner1').addEventListener('click', innerClickDown, true)
        document.getElementById('inner1').addEventListener('click', innerClickUp)

        function outterClickDown(e) {
            console.log('outterDown', e.target, this, e.target === this)
        }

        function innerClickDown(e) {
            console.log('innerDown', e.target, this, e.target === this)
        }

        function innerClickUp(e) {
            event.stopPropagation();

            console.log('innerUp', e.target, this, e.target === this)
        }

        function outterClickUp(e) {
            console.log('outterUp', e.target, this, e.target === this)
        }
    </script>
</body>

注意

  1. 该方法只会阻止事件继续向后传播,当前元素上当前阶段的函数还是会被事件触发(比如绑定了多个函数)
  2. 在最深层dom上,无论是捕获还是冒泡阶段调用这个方法,冒泡阶段的函数都一定会被触发
  3. 改用e.stopImmediatePropagation()可以改变上面两个效果

绑定事件监听的三种方法

  1. HTML的on属性(attribute): 注意在react中不要写函数调用的小括号,但在原生HTML中要写小括号。 使用该方法函数只会在事件冒泡阶段被触发
<body>
   <div onclick="myclick()">
       <button onclick="console.log(1)">点击</button>
   </div>
   <script>
       function myclick() {
           console.log('2')
       }
   </script>
</body>

上面效果等同于element.setAttribute()

<body>
    <div id="outer">
        <button onclick="console.log(1)">点击</button>
    </div>
    <script>
        document.getElementById('outer').setAttribute('onclick', 'myclick()')

        function myclick() {
            console.log('2')
        }
    </script>
</body>
  1. dom元素的事件属性
<body>
    <div id="outer">
        <button onclick="console.log(1)">点击</button>
    </div>
    <script>
        document.getElementById('outer').onclick = myclick
        function myclick() {
            console.log('2')
        }
    </script>
</body>

注意这个时候onclick的值是一个函数对象,不用加小括号。

这种方法和上面一样,监听函数都只能在事件冒泡阶段被触发。

  1. dom元素addEventListener
<body>
    <div id="outer">
        <button onclick="console.log(1)">点击</button>
    </div>
    <script>
        document.getElementById('outer').addEventListener('click', myclick, true)
        function myclick() {
            console.log('2')
        }
    </script>
</body>

addEventListener接收三个参数,分别是事件名,函数名,一个布尔值。

最后一个布尔值表示这个函数会在事件捕获阶段还是事件冒泡阶段被触发,true为捕获阶段,false为冒泡阶段,默认是false。

事件捕获和事件冒泡

常见事件

鼠标移动

  • mouseenter:当鼠标移入某元素时触发。(没有冒泡)
  • mouseleave:当鼠标移出某元素时触发。(没有冒泡)
  • mouseover:当鼠标移入某元素时触发,移入和移出其子元素时也会触发。
  • mouseout:当鼠标移出某元素时触发,移入和移出其子元素时也会触发。
  • mousemove:鼠标在某元素上移动时触发,即使在其子元素上也会触发。
<!DOCTYPE html>
<html>

<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <style>
        .a {
            width: 300px;
            height: 300px;
            background: rgb(149, 206, 255);
        }

        .b {
            width: 200px;
            height: 200px;
            background: beige;
        }

        .c {
            width: 100px;
            height: 100px;
            background: violet;
        }
    </style>
</head>

<body>
    <div class="a">A
        <div class="b" onmouseenter="mouseenter()" onmouseleave="mouseleave()" onmouseout="mouseout()"
            onmouseover="mouseover()" onmousemove="mousemove()">B
            <div class="c">C
            </div>
        </div>
    </div>
    <script>
        function mouseenter() {
            // console.log('mouseenter')
        }

        function mouseleave() {
            // console.log('mouseleave')
        }

        function mouseout() {
            // console.log('mouseout')
        }

        function mouseover() {
            console.log('mouseover')
        }

        function mousemove() {
            // console.log('mousemove')
        }
    </script>
</body>

</html>

移动端事件

特有的touchstart/touchmove/touchend/touchcancacel

同时也具有click

但是tap/swipe不是原生的移动端事件,是根据touch事件封装的

点击延迟

由于双击是放大操作,对点击事件做了防抖,所以移动端的click会比touchstart/pc端的click慢300ms

点击穿透

事件不是dom产生的,而是浏览器派发的,由于点击延迟的效应,在一次touchstart后300ms会再派发一个click事件

假如页面上有两个元素A和B。B元素在A元素之上。我们在B元素的touchstart事件上注册了一个回调函数,该回调函数的作用是隐藏B元素。我们发现,当我们点击B元素,B元素被隐藏了,随后,A元素触发了click事件。