通过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日

相关文章

  • 封装获取dom元素的简单实例

    封装获取DOM元素的简单实例可以用以下步骤完成: 步骤1:选择DOM元素 首先,我们需要选择DOM元素。有几种选择DOM元素的方法: 通过ID选择 使用 document.getElementById() 方法通过ID选择一个DOM元素。例如: const myElement = document.getElementById(‘my-id’); 这将返回一…

    JavaScript 2023年6月10日
    00
  • JS实现从对象获取对象中单个键值的方法示例

    要从一个对象中获取单个键值,可以使用 JavaScript 的点(.)或中括号([])运算符。这两种方法可以通过 JavaScript 对象来访问属性值(键值)。 以下是其中一种实现方法: 方法一:使用点运算符获取单个键值 这是获取单个键值的最常见方法。可以通过将点运算符后跟键名称来引用对象中的特定键。示例如下: const object = { key1:…

    JavaScript 2023年6月10日
    00
  • Javascript之面向对象–方法

    下面是Javascript面向对象方法的完整攻略。 什么是面向对象 在开始讲解面向对象方法之前,需要先了解什么是面向对象。面向对象编程(Object Oriented Programming,OOP)是一种软件开发的方法和思想,它以对象为基础,通过封装、继承、多态等特性实现代码的灵活复用、维护和拓展。在Javascript中,我们可以通过构造函数和原型链来实…

    JavaScript 2023年5月18日
    00
  • 微信小程序 触控事件详细介绍

    微信小程序 触控事件详细介绍 在微信小程序开发中,触控事件是非常重要的一部分,掌握触控事件可以让我们更好地掌控页面的交互体验。接下来,我们将详细介绍微信小程序中常用的触控事件。 原生触控事件 微信小程序中,原生支持的触控事件有: touchstart 当手指触摸屏幕并开始移动时触发,即手指触摸屏幕的瞬间会触发一次。可以通过 event.touches 事件对…

    JavaScript 2023年6月11日
    00
  • JavaScript函数防抖与函数节流的定义及使用详解

    JavaScript函数防抖与函数节流的定义及使用详解 函数防抖和函数节流是 JavaScript 常用的两种优化方案。它们可以延迟函数的执行,减少频繁请求和高频事件造成的性能问题。 一、函数防抖 在 JavaScript 中,如果频繁触发某个事件,比如输入框输入,鼠标滚动等,可能会造成函数频繁执行,影响性能。而使用函数防抖可以延迟函数的执行,只有等到一段时…

    JavaScript 2023年5月27日
    00
  • JavaScript中0、空字符串、’0’是true还是false的知识点分享

    当JavaScript中使用布尔类型时,0、空字符串、’0’三者在布尔类型中都代表false。但是在某些场景下,它们会被解释成true。下面是关于这些场景的详细讲解: 0 在JavaScript中,数字0代表false。但是,在进行逻辑非操作符“!”运算时,0会被解释成true,因为它不是布尔类型,而是数值类型。例如: console.log(!0) // …

    JavaScript 2023年5月28日
    00
  • js opener的使用详解

    JavaScript中的opener 在JavaScript中,window.opener是一个全局对象,它代表调用当前窗口的父窗口对象。即如果我们使用一个子窗口来打开一个页面,那么该页面中的window.opener就代表了该子窗口的父窗口对象。opener对象的使用非常灵活,提供了多种用法。下面我们来详细了解一下opener对象。 属性 window.o…

    JavaScript 2023年6月11日
    00
  • JS对象创建的几种方式整理

    JS对象创建的几种方式整理的攻略如下: 1. 对象字面量方式 对象字面量方式是指直接使用 {} 创建对象,使用键值对的方式来描述对象的属性和属性值。示例如下: const person = { name: "Alice", age: 25, sayHi: function() { console.log("Hi, I’m &qu…

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