有关于js事件处理机制的总结

JS事件机制

简述JavaScript及其在浏览器中的地位

浏览器主要构成

1.User Interface(用户界面):所谓用户界面,就是通过浏览器渲染出来,让用户可见的界面,如地址栏,书签菜单栏等;

2.Browser Engine(浏览器引擎):主要操作呈现的引擎界面;

3.Rendering Engine(渲染引擎):负责渲染响应请求内容,如负责解析HTML和CSS;(与js解析器冲突,所以js执行时阻塞页面渲染)

4.Networking(网络):负责网络呼叫处理,如http请求;

5.JS Interpreter(JavaScript 解释器):负责解析和执行javascript代码;

6.UI Back(UI后端):用于绘制组合框和窗口等基本组建;

7.Data Persistence(数据持久):通常用来持久化存储少量数据,如cookie等;

有关于js事件处理机制的总结

JavaScript在浏览器中的地位

如上图,javascript处于浏览器中的核心位置,负责解释和执行js脚本,内置于浏览器中,通过浏览器提供的API来访问。

JavaScript构成

关于javascript的构成,大致可归结为三个部分:ECMAScript,DOM和BOM。

1.ECMAScript是对js的约束和规范,如基本语法结构;

2.DOM文档对象模型,是交互的资源,如html文档;

3.BOM浏览器对象模型,主要是对浏览器本身描述,如浏览器名称,版本号等;

有关于js事件处理机制的总结

JavaScript基本执行原理

单线程异步。下图很好地表述该过程。

所有的执行函数统一放在队列中进行排队。

一篇带你弄清楚JS运行机制--EventLoop

有关于js事件处理机制的总结

事件流

所谓事件流,也可理解为事件的轨迹。一般地,将事件流分为三个阶段:捕获阶段,目标阶段和冒泡阶段。

有关于js事件处理机制的总结

下图为三个阶段的大致流程图。

有关于js事件处理机制的总结

捕获阶段

捕获阶段处于事件流的第一阶段,该阶段的主要作用是捕获截取事件。在DOM中,该阶段始于Document,结束于body。即:从最外层的祖先元素,从外向内对目标元素进行事件的捕获(当然,在现在的很多高版本浏览器中,该过程结束于目标元素,只不过不执行目标元素而已,这也体现了目标元素具有双重范围)。

有关于js事件处理机制的总结

目标阶段

目标阶段处于事件流的第二阶段,该阶段的主要作用是执行绑定事件。一般地,该阶段具有双重范围,即捕获阶段的结束,冒泡阶段的开始;即:事件捕获到目标元素,捕获结束开始在目标元素上触发事件

冒泡阶段

冒泡阶段处于事件流的第三阶段,该阶段的主要作用是将目标元素绑定事件执行的结果返回给浏览器,处理不同浏览器之间的差异,主要在该阶段完成。即:事件从目标元素向他的祖先元素传递,依次触发祖先元素上的事件

有关于js事件处理机制的总结

三阶段在Dom中的完整流程

有关于js事件处理机制的总结

事件处理程序

有关于js事件处理机制的总结

html事件处理程序

所谓html事件处理程序,就是在dom结构中嵌套js代码。在html中,元素支持的所有事件,都可以使用与相应事件处理程序同名的html特性来指定,这个特性的值应该是能执行的js代码。

如点击事件。

<body>
 
<!--html事件处理程序-->
 
<input type="button" value="请点击1" onclick="HtmlEventHandlerProc()"/>

 
<!--每个function()存在一个局部变量,即事件对象event,通过event变量,可以直接访问事件对象。-->
 
<input type="button" value="请点击2" onclick="alert(event.type)"/>

 
<!--在函数内部,this值等于事件的目标元素。-->
 
<input type="button" value="请点击3" onclick="alert(this.value)"/>

 
<!--如果当前元素是一个表单元素,则作用域还会包含访问表单元素(父元素)的入口。
  让事件处理程序更快捷访问表单其他字段
(无需引用表单元素就能访问)-->
 
<form>
    <
input type="text" name="userName" value="Alan_beijing" />
    <
input type="button" value="测试表单元素" onclick="alert(userName.value)" />
  </
form>
</
body>
<
script>
 
function HtmlEventHandlerProc() {
    alert(
'测试html事件处理程序!!');
  }
</
script>

Tip:

1.事件处理程序中的代码,能够访问全局作用域中的任何变量;

2.每个function()存在一个局部变量,即事件对象event,通过event变量,可以直接访问事件对象。

3.在函数内部,this值等于事件的目标元素

4.如果当前元素是一个表单元素,则作用域还会包含访问表单元素(父元素)的入口。让事件处理程序更快捷访问表单其他字段(无需引用表单元素就能访问)

缺点

  1. 时差问题

      因为用户可能会在HTML元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件

  1. 扩展的作用域链在不同浏览器中会导致不同结果

不同js引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错

  1. html代码与js代码高度耦合

更换事件处理程序改动较大。

DOM0级事件处理程序

DOM0级事件很好地解决了html和js代码强耦合的问题。

1.为元素绑定事件

var btn = document.getElementById('myBtn');

btn.onclick = function () {

    alert('Clicked');

}

2.为元素解除事件

btn.onclick = null;

DOM2级事件处理程序

DOM2级事件定义了两个方法来为目标元素绑定事件处理程序(addEventListener())和解除事件处理程序(removeEventListener()),所有节点中都包含这两个方法,并且他们都接收三个参数:

要处理的事件名,事件处理程序和一个布尔值(true表示是在捕获阶段进行,false表示在冒泡阶段进行,默认false,冒泡阶段处理)

  1. 为事件添加click事件处理程序(冒泡阶段进行)
  2. 添加多个事件处理程序
  3. 移除事件处理程序

通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除,移除时,传入的参数与添加的程序时使用的参数相同

(这意味着匿名函数不能通过removeEventListener()来删除)

匿名函数不能删除,非匿名函数能删除

<script>
 
/*DOM2级事件定义了两个方法来为
  目标元素绑定事件处理程序
(addEventListener())和解除事件处理程序(removeEventListener())
  所有节点中都包含这两个方法,并且他们都接收三个参数:
  要处理的事件名,事件处理程序和一个布尔值
(true表示是在捕获阶段进行,false表示在冒泡阶段进行,默认冒泡10)*/
 
// 1.为事件添加click事件处理程序(冒泡阶段进行)
 
var btn = document.getElementById("myBtn");
 
btn.addEventListener("click", function () {
    alert(
this.id);
  },
false);

 
// 2.添加多个事件处理程序
 
btn.addEventListener("click", function myfunction() {
    alert(
"添加的第二个事件处理程序");
  },
false);

 
//3.移除事件处理程序
 
/*通过addEventListener()添加的事件处理陈旭只能使用removeEventListener()来移除,
  移除时,传入的参数与添加的程序时使用的参数相同
  (这意味着匿名函数不能通过
removeEventListener()来删除)
  匿名函数不能删除,非匿名函数能删除
*/
 
//不能删除,因为是匿名函数
 
btn.removeEventListener("click", function () {
    alert(
this.id);
  },
false);

 
//非匿名函数
 
btn.addEventListener("click", handler, false);
 
//删除事件处理程序
 
btn.removeEventListener("click", handler, false);
 
function handler() {}
</
script>

IE事件处理程序

IE提供了两个方法来绑定和卸载事件处理程序,attachEvent()和detachEvent(),这两个方法均接收两个参数,即事件处理程序名称和事件处理程序函数,并且在冒泡阶段添加(IE8及更早版本只支持冒泡)

  1. 为目标按钮添加绑定事件
  2. 为目标按钮添加绑定事件,多个执行绑定事件的结果是倒过来的。
  3. 为目标元素移除事件处理程序,注意:匿名事件处理程序是不能够移除的
  4. attachEvent和detachEvent默认this为window,需要call改变this指向

<script>
 
/*IE提供了两个方法来绑定和卸载事件处理程序,attachEvent()detachEvent()
  这两个方法均接收两个参数,即事件处理程序名称和事件处理程
  序函数,并且在冒泡阶段添加(
IE8及更早版本只支持冒泡)*/
 
// 1.为目标按钮添加绑定事件
 
var btn = document.getElementById("myBtn");
 
btn.attachEvent("onclick", function () {
    alert(
"IE事件处理程序!!");
  });
 
// 2.为目标按钮添加绑定事件,多个执行绑定事件的结果是倒过来的。
 
var btn = document.getElementById("myBtn");
 
btn.attachEvent("onclick", function () {
    alert(
"IE事件处理程序1!!");
  });

 
btn.attachEvent("onclick", function () {
    alert(
"IE事件处理程序2!!");
  });
 
//3.为目标元素移除事件处理程序  注意:匿名事件处理程序是不能够移除的
 
var btn = document.getElementById("myBtn");
 
var handler = function () {
    alert(
"IE事件处理程序");
  };
   
//绑定事件
 
btn.attach("onclick", handler);
   
//移除事件
 
btn.detachEvent("onclick", handler);
</
script>

跨浏览器事件处理程序

在跨浏览器中,无非就是三种选择:DOM0级选择(不常用,基本被废弃),DOM2级选择和IE选择(不常用,IE8及以下版本)。

<script>
 
//定义EventUtil
 
var EventUtil = {
   
addHandler: function(element, type, handler){
     
if (element.addEventListener){
        element.
addEventListener(eventName, handler, false);
      }
else if(element.attachEvent) {
        element.
attachEvent("on" + type, function () {
          handler.
call(element);
        });
      }
else {
        element[
"on" + type] = handler;
      }
    },
   
removeHandler: function(element, type, handler){
     
if (element.removeEventListener){
        element.
removeEventListener(type, handler, false);
      }
else if(element.detachEvent){
        element.
detachEvent("on" + eventName, function () {
          handler.
call(element);
        });
      }
else {
        element[
"on" + type] = null;
      }
    }
  };

 
//调用
 
var btn = document.getElementById("myBtn");
 
var handler = function () {
    alert(
"事件处理程序跨浏览器!!");
  };
 
//绑定事件处理程序
 
EventUtil.addHandler(btn, "click", handler);
 
//移除事件处理程序
 
EventUtil.removeHandler(btn, "click", handler);
</
script>

Ps:事件回调函数的作用域问题

    事件绑定函数时,该函数会以当前元素为作用域执行。

(1)使用匿名函数

    我们为回调函数包裹一层匿名函数。包裹之后,虽然匿名函数的作用域被指向事件触发元素,但执行的内容就像直接调用一样,不会影响其作用域。

(2)使用 bind 方法

使用匿名函数是有缺陷的,每次调用都包裹进匿名函数里面,增加了冗余代码等,此外如果想使用 removeEventListener 解除绑定,还需要再创建一个函数引用。Function 类型提供了 bind 方法,可以为函数绑定作用域,无论函数在哪里调用,都不会改变它的作用域。

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的信息。

DOM中的事件对象

在触发DOM上的某个事件时,会在事件处理程序函数中会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。

event.type属性表示事件类型。

Event.currentTarget意思是事件处理程序当前正在处理事件的那个元素,target意思是事件的目标。在事件处理程序内部,对象this始终等于currentTarget的值,而target则只包含事件的实际目标。

Event.preventDefault()方法,阻止特定事件的默认行为。

event.stopPropagation()方法,立即停止事件在DOM层次中的传播,即取消进一步的事件捕获或冒泡。

IE中的事件对象

在使用 DOM0 级方法添加事件处理程序时,event 对象作为 window 对象的一个属性存在。如果是通过 HTML 特性指定的事件处理程序,那么还可以通过一个名叫 event 的变量来访问 event 对象 (与 DOM 中的事件模型相同)。IE 的 event 对象同样也包含与创建它的事件相关的属性和方法。

event.returnValue属性相当于DOM中的preventDefault()方法,只要将returnValue设置为false,就可以阻止默认行为

event.cancelBubble属性与DOM中的stopPropagation()方法作用相同,都是用来停止事件冒泡的。由于IE不支持事件捕获,因而只能取消事件冒泡,但stopPropagation()可以同时支持事件捕获和冒泡。

跨浏览器的事件对象

综合考虑DOM和IE中的事件对象,写出跨浏览器的事件对象,放在之前的EventUtil中。

var EventUtil = {
 
addHandler: function(element, type, handler){
   
// 省略了其他代码
 
},
 
getEvent: function(event){
   
return event? event: window.event;
  },
 
getTarget: function(event){
   
return event.target || event.srcElement;
  },

  //取消默认事件
  preventDefault: function(event){
   
if (event.preventDefault){
      event.
preventDefault();
    }
else {
      event.
returnValue = false;
    }
  },
 
removeHandler: function(element, type, handler){
   
// 省略了其他代码
 
},

  //停止冒泡
  stopPropagation: function(event){
   
if (event.stopPropagation){
      event.
stopPropagation();
    }
else {
      event.
cancelBubble = true;
    }
  }
};

深入理解DOM中的事件对象

事件类型

内容相对比较简单,这里就暂且列举主要内容,若有需要,会在下篇文章论述。

有关于js事件处理机制的总结

    Web浏览器中可能发生的事件有很多类型。如前所述,不同的事件类型具有不同的信息,而 "DOM3级事件" 规定了下列几类事件:

    UI事件:在用户与页面上的元素交互时触发;

    鼠标事件:当用户通过鼠标在页面上执行操作时触发;

    滚轮事件:当使用鼠标滚轮(或类似设备)时触发;

    文本事件:当在文档中输入文本时触发;

    键盘事件:当用户通过键盘在页面上执行操作时触发;

    合成事件:当为IME(Input Method Editor,输入法编辑器)输入字符时触发;

    变动 (mutation) 事件:当底层 DOM 结构发生变化时触发。

内存和性能

事件的冒泡(Bubble)

所谓的冒泡指的就是事件的向上传导,当后代元素上的事件被触发时,其祖先元素的相同事件也会被触发

在开发中大部分情况冒泡都是有用的,如果不希望发生事件冒泡可以通过事件对象来取消冒泡

event.cancelBubble = true;

event.stopPropagation();

移除事件处理程序

如果内存中保留大量无用的事件处理程序,会影响性能。所以一定要在不需要的时候及时移除事件处理程序。尤其注意以下情况:使用innerHTML删除带有事件处理程序的元素时,要先将事件处理程序设置为null。使用委托也可以解决这个问题,不直接将事件加载会被innerHTML替换的元素,而是将事件赋给其父元素,这样就可以避免了。卸载页面时,最好手工清除所有的事件处理程序

事件委托

将事件统一绑定给元素的共同的祖先元素,这样当后代元素上的事件触发时,会一直冒泡到祖先元素从而通过祖先元素的响应函数来处理事件。

优点:

  1. 利用了冒泡,通过委派可以减少事件注册,提高程序的性能。
  2. 利用事件冒泡,只指定一个事件处理程序,就可处理某一类型所有事件。
  3. 可以实现当新增子对象时无需再次对其绑定(动态绑定事件

<script type="text/javascript">
 
//JavaScript原生实现事件委托
 
var item1 = document.getElementById("goSomewhere");
 
var item2 = document.getElementById("doSomething");
 
var item3 = document.getElementById("sayHi");

 
document.addEventListener("click", function (event) {
   
var target = event.target;
    switch (target.id) {
     
case "doSomething":
       
document.title = "事件委托";
       
break;
     
case "goSomewhere":
       
location.href = "http://www.baidu.com";
       
break;
     
case "sayHi": alert("hi");
       
break;
    }
  })
 
//jQuery事件delegate()实现事件委托
 
$(document).ready(function () {
    $(
"#myLinks").delegate("#goSomewhere", "click", function () {
      
location.href = "http://www.baidu.com";
    });
  });
</
script>

参考

浅谈JavaScript之事件(上)

深入理解DOM中的事件对象

JS事件机制

JavaScript事件代理(事件委托)

附注

防抖(debounce) 和 节流(throttling)

原因

以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。

  1. window对象的resize、scroll事件
  2. 拖拽时的mousemove事件
  3. 射击游戏中的mousedown、keydown事件
  4. 文字输入、自动完成的keyup事件

实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debounce和throttle两种解决办法。

throttle(节流)和debounce(防抖)其实都是函数调用频率的控制器

防抖(debounce)

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

根据执行的动作的触发位置分为:延迟debounce和前缘debounce

延迟debounce:当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。

前缘debounce:即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,并重新设定周期。

延迟debounce

触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

有关于js事件处理机制的总结

function debounce(func, wait) {

    let timeout;

    return function () {

        let context = this;

        let args = arguments;

//周期内触发事件无效处理

        if (timeout) clearTimeout(timeout);

       

        timeout = setTimeout(() => {

            func.apply(context, args)

        }, wait);

    }

}

//使用

content.onmousemove = debounce(count,1000);

PS:this 和 参数的传递

let context = this;

let args = arguments;

防抖函数的代码使用这两行代码来获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。

前缘debounce

触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。如果在 n 秒内又触发了事件,则会重新计算下次执行函数时间

有关于js事件处理机制的总结

function debounce(func,wait) {

    let timeout;

    return function () {

        let context = this;

        let args = arguments;

//周期内触发事件无效处理

        if (timeout) clearTimeout(timeout);

 

        timeout = setTimeout(() => {

            timeout = null;

        }, wait)

 

        if (!timeout) func.apply(context, args)

    }

}

双剑合璧

/**

 * @desc 函数防抖

 * @param func 函数

 * @param wait 延迟执行毫秒数

 * @param immediate true 表立即执行,false 表非立即执行

 */

function debounce(func,wait,immediate) {

    let timeout;

 

    return function () {

        let context = this;

        let args = arguments;

 

        if (timeout) clearTimeout(timeout);

        if (immediate) {

            timeout = setTimeout(() => {

                timeout = null;

            }, wait)

            if (!timeout) func.apply(context, args)

        }

        else {

            timeout = setTimeout(function(){

                func.apply(context, args)

            }, wait);

        }

    }

}

节流(throttle)

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。

根据实现方式分为:时间戳版和定时器版。

时间戳版的函数触发是在时间段内开始的时候

定时器版的函数触发是在时间段内结束的时候。

时间戳版

时间段内开始的时候触发执行动作

function throttle(func, wait) {

    let previous = 0;

return function() {

        let context = this;

    let args = arguments;

 

        let now = Date.now();

        if (now - previous > wait) {

            func.apply(context, args);

            previous = now;

        }

    }

}

定时器版

时间段内结束的时候触发执行动作

function throttle(func, wait) {

    let timeout;

    return function() {

        let context = this;

        let args = arguments;

        if (!timeout) {

            timeout = setTimeout(() => {

                timeout = null;

                func.apply(context, args)

            }, wait)

        }

 

    }

}

双剑合璧版

/**

 * @desc 函数节流

 * @param func 函数

 * @param wait 延迟执行毫秒数

 * @param type 1 表时间戳版,2 表定时器版

 */

function throttle(func, wait ,type) {

    if(type===1){

        let previous = 0;

    }else if(type===2){

        let timeout;

    }

    return function() {

        let context = this;

        let args = arguments;

        if(type===1){

            let now = Date.now();

 

            if (now - previous > wait) {

                func.apply(context, args);

                previous = now;

            }

        }else if(type===2){

            if (!timeout) {

                timeout = setTimeout(() => {

                    timeout = null;

                    func.apply(context, args)

                }, wait)

            }

        }

    }

}

参考

函数防抖和节流

移动端延迟300ms

  • 前言

移动端浏览器提供一个特殊的功能:双击(double tap)缩放

  • 移动端延迟300ms的原因

为什么要用触摸事件?触摸事件是移动端浏览器特有的html5事件。

移动端的click有很大延迟(大约300ms),300ms延迟来自判断双击和长按,因为只有默认等待时间结束以确定没有后续动作发生时,才会触发click事件。而触摸事件的延迟则是非常短的,使用触摸事件的能够提高页面响应速度,带来更好的用户体验。

重点:由于移动端会有双击缩放的这个操作,因此浏览器在click之后要等待300ms,看用户有没有下一次点击,也就是这次操作是不是双击。

  • 浏览器开发商的解决方案

方案一:禁用缩放

当HTML文档头部包含如下meta标签时:

<meta name="viewport" content="user-scalable=no">
<
meta name="viewport" content="initial-scale=1,maximum-scale=1">

 

表明这个页面是不可缩放的,那双击缩放的功能就没有意义了,此时浏览器可以禁用默认的双击缩放行为并且去掉300ms的点击延迟

缺点:就是必须通过完全禁用缩放来达到去掉点击延迟的目的,但只是想禁掉默认的双击缩放行为,这样就不用等待300ms来判断当前操作是否是双击。但是通常情况下,我们还是希望页面能通过双指缩放来进行缩放操作,比如放大一张图片,放大一段很小的文字。

方案二:更改默认的视口窗口

移动端浏览器默认的视口宽度!=设备浏览器视窗宽度,视口宽度>设备宽度大,视口宽度通常是980px。

为了让桌面站点能在移动端浏览器正常显示,可以通过以下标签来设置视口宽度为设备宽度。

<meta name="viewport" content="width=device-width">

对移动端适配和优化,就不需要双击缩放了。如果能够识别出一个网站是响应式的网站,那么移动端浏览器就可以自动禁掉默认的双击缩放行为并且去掉300ms的点击延迟。

这个方案相比方案一的好处在于,它没有完全禁用缩放,而只是禁用了浏览器默认的双击缩放行为,但用户仍然可以通过双指缩放操作来缩放页面

方案三:css 的 touch-action

除了IE之外的大部分浏览器都不支持这个新的CSS属性。touch-action这个CSS属性。这个属性指定了相应元素上能够触发的用户代理(也就是浏览器)的默认行为。如果将该属性值设置为touch-action: none,那么表示在该元素上的操作不会触发用户代理的任何默认行为,就无需进行300ms的延迟判断。

  • 代码解决方案

方案一:指针事件polyfill

除了IE,其他大部分浏览器都还不支持指针事件。有一些JS库,可以让我们提前使用指针事件。比如:

(1)谷歌的Polymer

(2)微软的HandJS

(3)@Rich-Harris 的 Points

关心的不是指针事件,而是与300ms延迟相关的CSS属性touch-action。由于除了IE之外的大部分浏览器都不支持这个新的CSS属性,所以这些指针事件的polyfill必须通过某种方式去模拟支持CSS属性touch-action。一种方案是JS去请求解析所有的样式表,另一种方案是将touch-action作为html标签的属性。

方案二:FastClick

FastClick是FT Labs专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。FastClick的实现原理是在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后的click事件阻止掉。

if ('addEventListener' in document) {
 
document.addEventListener('DOMContentLoaded', function()      {
    FastClick.attach(
document.body);
  },
false);
}

点击穿透问题

起因

既然click点击有300ms的延迟,那对于触摸屏,我们直接监听touchstart事件不就好了吗?

使用touchstart去代替click事件有两个不好的地方

第一:touchstart是手指触摸屏幕就触发,有时候用户只是想滑动屏幕,却触发了touchstart事件,这不是我们想要的结果;

第二:使用touchstart事件在某些场景下可能会出现点击穿透的现象。

点击穿透现象

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

这是因为在移动端浏览器,事件执行的顺序是touchstart > touchend > click。而click事件有300ms的延迟,当touchstart事件把B元素隐藏之后,隔了300ms,浏览器触发了click事件,但是此时B元素不见了,所以该事件被派发到了A元素身上。如果A元素是一个链接,那此时页面就会意外地跳转。

点击穿透现象3种情况

(1)点击蒙层穿透问题:点击蒙层(mask)上的关闭按钮,蒙层消失后发现触发了按钮下面元素的click事件。

(2)跨页面点击穿透问题:如果按钮下面恰好是一个有href属性的a标签,那么页面就会发生跳转因为 a标签跳转默认是click事件触发 ,所以原理和上面的完全相同

(3)点击新页穿透问题:这次没有mask了,直接点击页内按钮跳转至新页,然后发现新页面中对应位置元素的click事件被触发了。

解决方案

2种思路:

(1)不要混用touch和click。既然touch之后300ms会触发click,只用touch或者只用click就自然不会存在问题了。

(2)用掉(或者说是消费掉)touch之后的click。依旧用tap,只是在可能发生点击穿透的情形做额外的处理,拿个东西来挡住、或者tap后延迟350毫秒再隐藏mask、pointer-events、在下面元素的事件处理器里做检测(配合全局flag)

详细方案:

  1. 只用touch

最简单的解决方案,完美解决点击穿透问题。

把页面内所有click全部换成touch事件 touchstart 、’touchend’、’tap’, 需要特别注意 a标签,a标签的href也是click,需要去掉换成js控制的跳转,或者直接改成span + tap控制跳转。

  1. 只用click

下下策 ,因为会带来300ms延迟,页面内任何一个自定义交互都将增加300毫秒延迟,想想都慢。不用touch就不会存在touch之后300ms触发click的问题。

  1. 拿个东西挡住

比较笨的方法,千万不要用。

  1. tap后延迟350ms再隐藏mask

改动最小,缺点是隐藏mask变慢了,350ms还是能感觉到慢的。

  1. pointer-events

比较麻烦且有缺陷,不建议使用。mask隐藏后,给按钮下面元素添上 pointer-events: none; 样式,让click穿过去,350ms后去掉这个样式,恢复响应。缺陷是mask消失后的的350ms内,用户可以看到按钮下面的元素点着没反应,如果用户手速很快的话一定会发现。

  1. 在下面元素的事件处理器里做检测(配合全局flag)

比较麻烦,不建议使用。全局flag记录按钮点击的位置(坐标点),在下面元素的事件处理器里判断event的坐标点,如果相同则是那个可恶的click,拒绝响应。

  1. fastclick

好用的解决方案,不介意多加载几KB的话, 不建议使用 ,因为有人遇到了bug,

首先引入fastclick库,再把页面内所有touch事件都换成click,其实稍微有点麻烦,建议引入这几KB就为了解决点透问题不值得,不如用第一种方法呢。

参考

移动端300ms点击延迟和点击穿透问题