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

yizhihongxing

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

相关文章

  • JSON 客户端和服务器端的格式转换

    JSON(JavaScript 对象表示法)是一种轻量级数据交换格式,通常用于客户端与服务器端进行数据传输。在客户端和服务器端之间进行数据传输时,常常需要进行 JSON 格式的转换。接下来,我将为您提供一份详细的 JSON 客户端和服务器端的格式转换攻略。 JSON 格式转换 在进行 JSON 格式转换之前,我们首先需要了解两种形式的数据表示方法: JSON…

    JavaScript 2023年5月27日
    00
  • vue3:vue2中protoType更改为config.globalProperties问题

    在Vue.js 3中,一些API的使用方式发生了更新。其中,一个重要的改变是将Vue 2.x中的全局对象$和prototype更改为了config.globalProperties,以便更好的支持TypeScript类型和减少变量泄漏的问题。 下面是完整攻略: 1. 理解问题 在Vue.js 2.x 版本中,我们可以通过以下方式为Vue实例添加全局属性: V…

    JavaScript 2023年6月11日
    00
  • 深入理解React Native核心原理(React Native的桥接(Bridge)

    深入理解React Native核心原理之桥接(Bridge) React Native是一种基于React的JS框架,它可以让你使用JavaScript和React的开发方式来构建iOS和Android的原生应用。这些原生应用实际上是通过React Native桥接(Bridge)在JavaScript和iOS/Android平台之间进行通信和交互的。 什…

    JavaScript 2023年6月11日
    00
  • JavaScript事件类型中焦点、鼠标和滚轮事件详解

    JavaScript事件类型中焦点、鼠标和滚轮事件详解 JavaScript作为网页交互的基础语言,提供了一系列的事件类型来处理用户交互操作。其中焦点事件、鼠标事件和滚轮事件是常见的事件类型,本文将详细讲解这些事件类型及其应用。 焦点事件 在HTML页面中,有许多表单元素比如input、textarea等,当用户对这些元素进行操作时,就会触发焦点事件。常见的…

    JavaScript 2023年6月11日
    00
  • javascript unicode与GBK2312(中文)编码转换方法

    下面是详细讲解“javascript unicode与GBK2312(中文)编码转换方法”的完整攻略。 了解Unicode与GBK2312编码 在进行编码转换前,我们需要先了解所涉及的两种编码方式:Unicode和GBK2312。 Unicode是国际标准化组织制定的国际编码标准,它为世界上所有的字符规定了统一的编码,包括字母、数字、标点符号、各国文字等。U…

    JavaScript 2023年5月20日
    00
  • 详解JavaScript的函数简介

    详解JavaScript的函数简介 在 JavaScript 中,函数是一种重要的概念。函数是将代码封装成一个可执行的容器,可以通过调用函数来执行其中的代码。本文将详细介绍 JavaScript 函数的基本语法、定义方式、参数传递、值返回和作用域。 函数的基本语法 函数有以下基本语法: function functionName(parameters) { …

    JavaScript 2023年5月17日
    00
  • 用js模拟JQuery的show与hide动画函数代码

    下面是用JavaScript模拟jQuery的show和hide的完整攻略,步骤如下: 1. 获取元素并设置样式 首先,我们需要获取到要显示或隐藏的元素,并设置样式。我们可以使用document.querySelector或document.querySelectorAll获取元素,设置元素的display属性来控制元素的显示或隐藏。 const eleme…

    JavaScript 2023年6月11日
    00
  • JavaScript实现横线提示输入验证码随输入验证码输入消失的方法

    要实现这个功能,我们需要用到JavaScript和CSS。 首先,我们需要在HTML页面中添加一个input标签来接受验证码输入,同时在输入框下面添加一个div标签来显示横线提示。例如: <label for="code">请输入验证码:</label> <input type="text&quot…

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