通过fastclick源码分析彻底解决tap“点透”

通过fastclick源码分析彻底解决tap“点透”

背景

在移动端开发过程中,常常会遇到“点透”的问题。例如,当一个元素的click事件和另一个元素的touchend事件同时被触发时,就会发生“点透”,相当于用户点了下下一层的元素。为了避免这种问题的出现,我们可以使用第三方库 fastclick 来解决这一问题,此处将通过 fastclick 的源码分析来解决这个问题。

fastclick 原理

fastclick 的核心是通过对 click 事件在移动设备上的延迟处理机制进行封装,启用 touch 事件模拟 click 事件并在合适的时候阻止默认行为从而彻底解决移动端的点透问题。在 fastclick 的实现中,主要依赖了 touchstart、touchend 和 click 事件,具体实现可以参考下面的代码:

var FastClick = function(layer, options) {
    var oldOnClick;

    options = options || {};

    // If necessary, set options to the global default settings
    for (var key in FastClick.defaults) {
        if (typeof options[key] === 'undefined') {
            options[key] = FastClick.defaults[key];
        }
    }

    // ......

    function onTouchStart(event) {
        if (event.targetTouches.length > 1) {
            return true;
        }

        if (options.disableInput) {
            event.preventDefault();
        }

        downEl = hasTouch ? event.target : (event.srcElement || element);
        addClass(downEl, options.activeClass);

        lastTouchStartTime = Date.now();

        clearTimeout(activeTimeout);

        return true;
    }

    // ......

    function onClick(event) {
        if (deviceIsIOS && !event.metaKey && !event.ctrlKey) {
            event.preventDefault();
        }
    }

    // ......

    function onMouseUp(event) {
        if (trackingClick) {
            targetElement = event.target || event.srcElement;
            targetTagName = targetElement.tagName.toLowerCase();
            removeClass(downEl, options.activeClass);
            trackingClick = false;

            if (targetTagName === 'label') {
                forElement = findControl(targetElement);
                if (forElement) {
                    focus(targetElement);
                    if (deviceIsAndroid) {
                        return false;
                    }
                    targetElement = forElement;
                }
            } else if (shouldUseClick(targetElement)) {
                doClick(targetElement, event);
                event.preventDefault();
            }

            return false;
        }

        return true;
    }

    // ......

    function onDoubleClick(event) {

        // Certain devices have specific user agent strings that require even more
        // hacky solutions to detect whether they are mobile. These are provided
        // in the big if clause which follows.
        if (deviceIsAndroid && (Date.now() - lastTouchEndTime) < 200) {
            // Trick the browser into believing that the user had actually
            // tapped on the target element by manually setting up touch coordinates
            // for it
            resetOnTouchEnd();

            targetElement = event.target || event.srcElement;
            oldTargetElement = targetElement;
            simulateEvent(targetElement, 'mousedown');

            // 延迟触发移除 class and preventDefault
            addClass(targetElement, options.activeClass);
            activeTimeout = setTimeout(function() {
                removeClass(targetElement, options.activeClass);
            }, options.activeTimeout);

            simulateEvent(targetElement, 'mouseup');
            simulateEvent(targetElement, 'click');

        }
    }

    // 为某个元素绑定事件
    function fastClick() {
        var layer = this;

        // 为特定 layer 添加 fastclick 特性
        addEvent(layer, 'touchstart', onTouchStart);
        addEvent(layer, 'touchmove', onTouchMove);
        addEvent(layer, 'touchend', onTouchEnd);
        addEvent(layer, 'touchcancel', onTouchCancel);
        addEvent(layer, 'mousedown', onMouseDown);
        addEvent(layer, 'mouseup', onMouseUp);
        addEvent(layer, 'mousemove', onMouseMove);
        addEvent(layer, 'click', onClick);
        addEvent(layer, 'dblclick', onDoubleClick);

        // ....

    }

    // Attach FastClick to object
    // 将 fastclick 挂载在某一个元素上
    FastClick.attach = function(layer, options) {
        return new FastClick(layer, options);
    };

    // ....

    // 绑定 fastclick
    if (typeof window.ontouchstart === 'undefined' && !deviceIsAndroid) {
        document.addEventListener('DOMContentLoaded', function() {
            FastClick.attach(document.body);
        }, false);
    }

    // ....

};

fastclick 实现

fastclick 本身比较简单,通过给一个对象的 layer 属性绑定事件,然后在事件处理的过程中阻止默认事件从而达到消除点透的目的。我们常常使用:

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

将 fastclick 应用到页面上来消除点透。但是,这并不能解决所有的问题。我们还需要解决:为什么在移动端环境下快速触发2次click事件时,会出现1次“点透”的问题。fastclick 的原理的概括如下:

  • 处理 touchstart 事件,如果目标元素是表单控件元素,就不处理,默认使用系统点击
  • 处理 touchmove 事件,记录当前不发布 click 事件,防止滑动时有误点击
  • 处理 touchend 事件,触发 click 事件,如果时间大于 300ms,认为不是快速点击,不处理不阻止
  • 处理 click 事件,阻止事件默认行为

以下是 fastclick 的使用示例:

<body>
  <div class="button">Click me!</div>
  <script src="//cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js"></script>
  <script>
    window.addEventListener('load', function() {
      FastClick.attach(document.body);
    }, false);
  </script>
</body>

解决方案

针对“为什么在移动端环境下快速触发2次click事件时,会出现1次“点透”的问题”,我们需要加入一个遮罩层来解决。点击一次时,在 300ms 内尝试拦截所有的后续点击事件,此时使用一个类为 in-acting 的遮罩层将当前点击目标元素覆盖,拦截所有的 click 事件,然后在 300ms 后移除遮罩层,释放 click 事件到目标元素。修改的具体实现可以参考下面的代码:

// 自定义 fastclick
var FC = function(layer){
  // ..
  var self = this;

  self.threshold = options.threshold || 10; // px
  self.layer = layer;

  self.reset = function(){
    self.trackingClick = false;
    self.trackingClickStart = 0;
    self.targetElement = null;
    self.lastTouchIdentifier = self.touchIdentifier;
    self.touchStartX = 0;
    self.touchStartY = 0;
    self.lastTouchMove = 0;
    self.touchBoundary = options.touchBoundary || 10;
    self.layer.removeEventListener('touchend', self, false);
    self.layer.removeEventListener('touchcancel', self, false);
    self.layer.removeEventListener('touchmove', self, false);
    document.removeEventListener('scroll', self, true);
  }
  // ..
  self.layer.addEventListener('touchstart', self, false);
  self.layer.addEventListener('touchmove', self, false);
  self.layer.addEventListener('touchend', self, false);
  self.layer.addEventListener('touchcancel', self, false);
};
FC.prototype.handleEvent = function(event){
  switch(event.type){
    case 'touchstart': return this.onTouchStart(event);
    case 'touchmove': return this.onTouchMove(event);
    case 'touchend': return this.onTouchEnd(event);
    case 'touchcancel': return this.onTouchCancel(event);
  }
}
FC.prototype.onTouchStart = function(event){
  console.log(event);
  if(this.trackingClick) return;
  this.trackingClick = true;
  this.targetElement = event.target;
  this.touchIdentifier = event.touches[0].identifier;
  this.trackingClickStart = Date.now();

  var self = this;
  this.layer.addEventListener('touchmove', self, false);
  this.layer.addEventListener('touchend', self, false);
  this.layer.addEventListener('touchcancel', self, false);
}
FC.prototype.onTouchEnd = function(event){
  console.log(event);
  if(!this.trackingClick) return;
  if((Date.now() - this.trackingClickStart) > 300) return this.reset();
  this.preventDefault(event);
  this.stopEventPropagation(event);
  this.reset();
  return false;
}
FC.prototype.onTouchMove = function(event){
  // console.log(event);
  if(!this.trackingClick) return true;
  if(this.lastTouchIdentifier !== event.touches[0].identifier) return this.reset();
  var dx = event.touches[0].pageX - this.touchStartX;
  var dy = event.touches[0].pageY - this.touchStartY;
  if((dx * dx + dy * dy) > (this.threshold * this.threshold)) return this.reset();
}
FC.prototype.onTouchCancel = function(event){
  console.log(event);
  this.preventDefault(event);
  this.stopEventPropagation(event);
  this.reset();
  return false;
}

FC.prototype.preventDefault = function(event){
  event.preventDefault();
}
FC.prototype.stopEventPropagation = function(event){
  event.stopPropagation();
}
var addCustomFastClick = function(){
  var inAction = false,
      isTouch = false;
  $('body').on('touchend', function(e){
    if(!isTouch){
      return;
    }
    inAction = true;
    $(e.target).addClass('in-acting');
    setTimeout(function(){
      $('body .in-acting').removeClass('in-acting');
      inAction = false;
    }, 300);
  }).on('touchstart', function(e){
    isTouch = true;
  }).on('touchmove', function(e){
    isTouch = false;
  });
  return;
};
addCustomFastClick();

我们自定义了一个 fastclick,实质类似于解决方案1,即阻止后续 300ms 内的事件(除了第一个事件)并添加遮罩层,此处的遮罩层使用的类是 in-acting。

解决方案1示例

<body>
  <div class="button">Click me!</div>
  <script src="//cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js"></script>
  <script>
    if ('addEventListener' in document) {
      document.addEventListener('DOMContentLoaded', function() {
          FastClick.attach(document.body);
      }, false);
    }

    // 自定义的处理遮罩层
    function addCustomFastClick() {
      var inAction = false,
          isTouch = false;
      $('body').on('touchend', function(e) {
          if (!isTouch) {
              return;
          }
          inAction = true;
          $(e.target).addClass('in-acting');
          setTimeout(function() {
              $('body .in-acting').removeClass('in-acting');
              inAction = false;
          }, 300);
      }).on('touchstart', function(e) {
          isTouch = true;
      }).on('touchmove', function(e) {
          isTouch = false;
      });
      return;
    }
    addCustomFastClick();
  </script>
</body>

解决方案2示例

<body>
  <div class="button">Click me!</div>
  <script>
    var fc = new FC(document.querySelector('.button'));
  </script>
</body>

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:通过fastclick源码分析彻底解决tap“点透” - Python技术站

(0)
上一篇 2023年6月11日
下一篇 2023年6月11日

相关文章

  • 贴一个在Mozilla中常用的Javascript代码

    关于“贴一个在Mozilla中常用的Javascript代码”的完整攻略,我可以从以下方面进行详细讲解: 1. 熟悉 Mozilla 平台环境 在 Mozilla 平台下编写 Javascript 代码,需要先熟悉它的基本环境,包括: Gecko 内核:Mozilla 平台使用 Gecko 解析 HTML、CSS 等标记语言,并执行 Javascript 脚…

    JavaScript 2023年6月10日
    00
  • js面向对象之静态方法和静态属性实例分析

    以下是“js面向对象之静态方法和静态属性实例分析”的完整攻略: 什么是静态方法和静态属性 在JavaScript中,静态方法和静态属性仅属于特定的类(构造函数),而不是属于类的实例。静态方法和静态属性的特点是在创建对象之前就已经存在,也就是说,它们可以不依赖对象而直接调用。 静态方法 静态方法是将函数绑定到一个类上,而不是将函数绑定到类的实例上。我们可以使用…

    JavaScript 2023年5月27日
    00
  • JSP页面间的传值方法总结

    JSP(JavaServer Pages)作为Web开发技术的重要组成部分,经常需要将一些变量数值或对象引用从一个JSP页面传递到另一个页面。本文总结了JSP页面间的传值方法,帮助开发者高效地处理这些场景。 一、JSP页面间的传值方法 1. 直接在URL中传递参数 对于两个页面直接的简单参数传递场景,可以在URL中携带参数。Servlet容器可以从HTTP请…

    JavaScript 2023年6月11日
    00
  • JS实现的视频弹幕效果示例

    下面是详细讲解“JS实现的视频弹幕效果示例”的完整攻略: 简介 视频弹幕效果是目前比较流行的一种视频播放方式,也可以为网页增加一些互动性。通过JS实现的视频弹幕效果,可以让用户在看视频时发表自己的评论或者观点,同时其他用户也可以看到这些弹幕,增加了互动性。 准备工作 安装编译环境 首先需要安装编译环境,包括node.js,npm,webpack等。如果您还不…

    JavaScript 2023年6月11日
    00
  • javascript中的注释使用与注意事项小结

    当我们编写Javascript代码时,除了编写实际的功能代码,还会添加注释来帮助我们理解代码并使别人也能理解代码。在本篇攻略中,我将详细讲解Javascript中注释的使用和注意事项。 注释的基本语法 Javascript支持两种类型的注释:单行注释和多行注释。 单行注释 单行注释用于在一行代码中添加注释。在单行注释的开头使用两个斜杠(//)表示,接着添加注…

    JavaScript 2023年6月11日
    00
  • javascript字符串替换函数如何一次性全部替换掉

    如何一次性全部替换掉JavaScript字符串中的一个子串,可以使用字符串方法replace()结合正则表达式,具体步骤如下: 将要替换掉的子串放在一个正则表达式中作为需要匹配的模式。 将要替换掉的子串放置在replace()方法的第二个参数中,这个参数可以是一个字符串或者一个函数。 可选地,在正则表达式中使用修饰符g来匹配多个子串,而不仅仅是第一个。 下面…

    JavaScript 2023年5月28日
    00
  • JavaScript实现的简单Tab点击切换功能示例

    以下是“JavaScript实现的简单Tab点击切换功能示例”的完整攻略: 理解Tab切换功能 在网页设计中,Tab切换功能是常见的交互方式,它可以在同一页面内切换不同的内容,提升用户体验。在实现Tab切换功能时,需要通过JavaScript脚本实现元素的显示和隐藏。 准备工作 在实现Tab切换功能之前,需要进行一些准备工作。其中最重要的是,需要确定需要切换…

    JavaScript 2023年6月10日
    00
  • JavaScript自动生成24小时时间区间

    首先介绍一下JavaScript自动生成24小时时间区间的原理:JavaScript中Date对象的getHours()和setHours()方法分别可以获取和设置时间,可以通过循环来生成24小时时间区间。 具体实现过程可以分为以下几步: 创建一个起始时间,如当前时间。可以使用new Date()创建Date对象表示当前时间。 循环24次,每次将起始时间的小…

    JavaScript 2023年5月27日
    00
合作推广
合作推广
分享本页
返回顶部