在移动端,网页上的点击穿透问题导致了非常糟糕的用户体验。那么该如何解决这个问题呢?

问题产生的原因

移动端浏览器的点击事件存在300ms的延迟执行,这个延迟是由于移动端需要通过在这个时间段用户是否两次触摸屏幕而触发放大屏幕的功能。那么由于click事件将延迟300ms的存在,开发者在页面上做一些交互的时候往往会导致点击穿透问题(可以能是层之间的,也可以是页面之间的)。

解决问题

之前遇到这个问题的时候,有在网上看了一些关于解决移动端点击穿透的问题,也跟着网上提出的方式进行了各项测试,最终还是觉得使用fastclick插件比较靠谱些,其他几种方法多多少少会存在一些其他问题(当然,fastclick也不是说完全兼容各项,但相对于其他一些方法不会造成较明显的问题)

使用方式:

<!-- 引入文件 -->
<script src="fastclick.js"></script>

js:

// fastclick 使用
/*
@params layer 需要处理click事件的视图
@params options 一些配置
{
touchBoundary: 10 // 点击事件边界线
tapDelay: 200 // tap最小延时
tapTimeout: 700 // tap最大延时
}
*/ FastClick.attach(layer,options) // 一般使用 FastClick.attach(document.body)

fastclick实现过程

首先,扔上注解文件:fastclick-read.js 中文注解文件 ,下面内容提取部分代码

1.拦截给定视图区域的各项事件,绑到layer处理

// Set up event handlers as required
// 配置需要用到的操作事件/给指定绑定各项事件监听
if (deviceIsAndroid) {
layer.addEventListener('mouseover', this.onMouse, true);
layer.addEventListener('mousedown', this.onMouse, true);
layer.addEventListener('mouseup', this.onMouse, true);
} layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchmove', this.onTouchMove, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);

2.判断是否需要对触发事件的标签生成一个针对该标签的click事件

// onTouchStart 代码行

// 注册click事件追踪
this.trackingClick = true;
this.trackingClickStart = event.timeStamp;
this.targetElement = targetElement; this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY; // 若干行代码... // 防止快速的两次tap导致的点透
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
} // onTouchMove 代码行 // 如果touch事件是移动的,取消点击事件跟踪
if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
this.trackingClick = false;
this.targetElement = null;
} // onTouchEnd 代码行 // 阻止快速双击导致触发第二次屏幕点击事件 (issue #36)
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
this.cancelNextClick = true;
return true;
} // 如果超出最大延时,事件继续执行
if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
return true;
} // 重新设置cancelNextClick,阻止input的事件被某些异常所取消 (issue #156)
this.cancelNextClick = false; this.lastClickTime = event.timeStamp; trackingClickStart = this.trackingClickStart;
this.trackingClick = false;
this.trackingClickStart = 0; // 舍去一些兼容处理的代码 // else if (this.needsFocus(targetElement)) 判断且满足needsFocus条件 如果点击元素是为了聚焦 this.focus(targetElement);
this.sendClick(targetElement, event); // 这里生成click // 判断且满足needsClick条件 如果点击元素是为了点击
// 阻止真实的点击继续执行 -- 除非该标签被标记为允许真实点击(class="needsclick")
if (!this.needsClick(targetElement)) {
event.preventDefault();
this.sendClick(targetElement, event); // 这里生成click
} // onTouchCancel 代码行 FastClick.prototype.onTouchCancel = function() {
this.trackingClick = false;
this.targetElement = null;
}; // onClick 代码行 FastClick.prototype.onClick = function(event) {
var permitted; // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44).
// In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
// 点击事件在被fastclick触发之前已经被其他类似fastclick功能的第三方代码库触发的情况下,尽早的为事件跟踪标签返回一个false值,同时也能够尽早结束onTouchEnd事件
if (this.trackingClick) {
this.targetElement = null;
this.trackingClick = false;
return true;
} // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks
// the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
// IOS的异常现象(issue #18) : 当表单内存在submit按钮,在ios模拟器点击"enter"或者在弹出键盘中点击"go",将会触发一次"伪装"成submit按钮的点击事件将表单提交
if (event.target.type === 'submit' && event.detail === 0) {
return true;
} permitted = this.onMouse(event); // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the
// browser's click doesn't go through.
// 如果不允许click,那么只设置targetElement。确保onMouse中!targetElement的判断结果有值,并且浏览器的点击失效
if (!permitted) {
this.targetElement = null;
} // If clicks are permitted, return true for the action to go through.
// 如果允许click,返回true用以点击动作的传递
return permitted;
}; // onMouse 代码行 FastClick.prototype.onMouse = function(event) { // If a target element was never set (because a touch event was never fired) allow the event
// 如果不存在targetElement(触摸事件未被触发),返回true
if (!this.targetElement) {
return true;
} if (event.forwardedTouchEvent) {
return true;
} // Programmatically generated events targeting a specific element should be permitted
// 代码触发的事件,并且针对有明确的元素,则返回true
if (!event.cancelable) {
return true;
} // Derive and check the target element to see whether the mouse event needs to be permitted;
// unless explicitly enabled, prevent non-touch click events from triggering actions,
// to prevent ghost/doubleclicks.
// 检查目标元素,确定鼠标事件是否需要被允许
// 除非明确指定事件可行,不然就阻止非点击事件,主要用于元素重叠情况下双击产生异常而触发不必要的事件。
if (!this.needsClick(this.targetElement) || this.cancelNextClick) { // Prevent any user-added listeners declared on FastClick element from being fired.
// 阻止fastclick元素上其他的定义的事件被触发
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
} else { // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
// hack 为了一些不支持Event#stopImmediatePropagation的客户端(如 Android 2)
event.propagationStopped = true;
} // Cancel the event
// 取消事件
event.stopPropagation();
event.preventDefault(); return false;
} // If the mouse event is permitted, return true for the action to go through.
// 如果允许该鼠标事件,返回true用以点击动作的传递
return true;
}; // sendClick 代码行 -- 生成click事件 // 模拟一次点击事件,同时添加上forwardedTouchEvent,表明可被跟踪
clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
clickEvent.forwardedTouchEvent = true;
targetElement.dispatchEvent(clickEvent);

以上就是重点的一些判断及实现的代码,可点击"github - fastclick-read 源码注释"理解更详细内容,建议动手测试,完整的跟一次click事件在fastclick代码内的执行流程。

源码中一些挺实用的基础知识点

事件冒泡和事件捕获

事件冒泡:事件在某个节点被触发,将会随着DOM树向上冒泡并根据当前节点是否满足冒泡触发的条件来进行同类型事件的触发,直至根节点(html)。

事件捕获:事件在根节点上被触发,开始向子元素传播并根据当前节点是否满足捕获触发的条件来进行同类型事件的触发,直至实际触发该事件的节点。

首先,我们给出页面结构:

<html>
<body>
<div class="div-outside">
<div class="div-inside">
<span class="span-click">a span for click</span>
</div>
</div>
</body>
</html>

addEventListener的第三个参数决定该事件是否在捕获阶段执行,Event.cancelBubble 属性值(true/false)决定该事件是否冒泡,推荐使用Event.stopPropagation()阻止冒泡

事件捕获执行及效果:

document.querySelector('.span-click').addEventListener('click',function(e){
console.log("span");
},!0);
document.querySelector('.div-inside').addEventListener('click',function(e){
console.log("inside");
},!0);
document.querySelector('.div-outside').addEventListener('click',function(e){
console.log("outside");
},!0);
document.body.addEventListener('click',function(e){
console.log("body");
},!0);
document.addEventListener('click',function(e){
console.log("html");
},!0); // 输出
/*
* html
* body
* outside
* inside
* span
*/

事件冒泡执行及效果:

document.querySelector('.span-click').addEventListener('click',function(e){
console.log("span");
},!1);
document.querySelector('.div-inside').addEventListener('click',function(e){
console.log("inside");
},!1);
document.querySelector('.div-outside').addEventListener('click',function(e){
console.log("outside");
},!1);
document.body.addEventListener('click',function(e){
console.log("body");
},!1);
document.addEventListener('click',function(e){
console.log("html");
},!1); // 输出
/*
* span
* inside
* outside
* body
* html
*/

事件触发时,哪个优先?

document.querySelector('.span-click').addEventListener('click',function(e){
console.log("span");
},!1);
document.querySelector('.div-inside').addEventListener('click',function(e){
console.log("inside");
},!0);
document.querySelector('.div-outside').addEventListener('click',function(e){
console.log("outside");
},!1);
document.body.addEventListener('click',function(e){
console.log("body");
},!1);
document.addEventListener('click',function(e){
console.log("html");
},!1); // 输出
/*
* inside
* span
* outside
* body
* html
*/

显而易见...

Event.stopPropagation 和 Event.stopImmediatePropagation

Event.stopPropagation:阻止事件向上传播(冒泡)

Event.stopImmediatePropagation:阻止该标签上的同类型事件被触发并阻止事件向上传播(冒泡)

html:

<body>
<div class="div-outside">
<div class="div-inside">
a div for click
</div>
</div>
</body>

不做任何处理的执行及效果:

document.querySelector('.div-inside').addEventListener('click',function(e){
console.log("inside first print");
},!1);
document.querySelector('.div-inside').addEventListener('click',function(e){
console.log("inside second print");
},!1);
document.querySelector('.div-outside').addEventListener('click',function(e){
console.log("outside");
},!1);
document.body.addEventListener('click',function(e){
console.log("body");
},!1); // 输出
/*
* inside first print
* inside second print
* outside
* body
*/

Event.stopPropagation执行及效果:

document.querySelector('.div-inside').addEventListener('click',function(e){
e.stopPropagation();
console.log("inside first print");
},!1);
document.querySelector('.div-inside').addEventListener('click',function(e){
console.log("inside second print");
},!1);
document.querySelector('.div-outside').addEventListener('click',function(e){
console.log("outside");
},!1);
document.body.addEventListener('click',function(e){
console.log("body");
},!1); // 输出
/*
* inside first print
* inside second print
*/

Event.stopImmediatePropagation执行及效果:

document.querySelector('.div-inside').addEventListener('click',function(e){
e.stopImmediatePropagation();
console.log("inside first print");
},!1);
document.querySelector('.div-inside').addEventListener('click',function(e){
console.log("inside second print");
},!1);
document.querySelector('.div-outside').addEventListener('click',function(e){
console.log("outside");
},!1);
document.body.addEventListener('click',function(e){
console.log("body");
},!1); // 输出
/*
* inside first print
*/

参考文档:

MDN Event.stopPropagation

MDN Event.stopImmediatePropagation

Window.getSelection()

该方法返回一系列选择文本的参数,如选择范围,字符当前索引等

html:

<div class="text first-text">hello world!</div>
<div class="text second-text">hello world!</div>

js:

var elems = document.querySelectorAll('.text');
var len = elems.length;
while(len){
elems[len-1].addEventListener('mouseup',function(){
console.log(window.getSelection());
},!1);
len--;
}

效果:

如上图所示,参数有:

· anchorNode:选择范围开始的node

· anchorOffset:anchorNode中的起始索引

· focusNode:选择范围结束的node

· focusOffset:focusNode中的结束索引

· isCollapsed:起始和结束是否在一个点,返回true/false

· rangeCount:选择段的段数,貌似一直为1段,尝试按住shift选择多段,然而并不行

· type:操作类型,如:选择:Range,插入符:Caret(input中) 等情况...

· 其他暂时不去深究,嘿嘿!!!

参考文档:

MDN Window.getSelection()

 事件操作 --- 创建 -> 配置 -> 派遣

如needsClick里的代码,创建一次不带任何handle的click事件,然后将该事件在指定元素上触发,以触发该元素上的同类型事件。

html:

<div id="click-one">click-one</div>
<div id="click-two">click-two</div>

比如,点击click-one,给click-two创建个click事件并执行,用以触发click-two上我们写的点击事件。

··· 方式一(MDN并不推荐,标明被移出web标准):

Document.creatEvent();

MouseEvent.initMouseEvent();

EventTarget.dispatchEvent();

js:

document.getElementById('click-one').addEventListener('click',function(e){
console.log("click-one");
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
document.getElementById('click-two').dispatchEvent(evt);
},!1);
document.getElementById('click-two').addEventListener('click',function(e){
console.log("click-two");
},!1); /*
语法.参数,值得注意的是最后一个参数,相关标签,mouseover和mouseout使用,其他情况传null
event.initMouseEvent(type, canBubble, cancelable, view,
detail, screenX, screenY, clientX, clientY,
ctrlKey, altKey, shiftKey, metaKey,
button, relatedTarget);
*/

···方式二(MDN推荐,但貌似兼容性暂时捉急):

new Event();

EventTarget.dispatchEvent();

js:

document.getElementById('click-one').addEventListener('click',function(e){
console.log("click-one");
var evt = new Event('click',{"bubbles":true, "cancelable":true});
document.getElementById('click-two').dispatchEvent(evt);
},!1);
document.getElementById('click-two').addEventListener('click',function(e){
console.log("click-two");
},!1);
/*
语法.参数
new Event(typeArg,eventInit);
typeArg:事件名称
eventInit:
bubbles 是否冒泡
cancelable 是否可被取消
scoped 是否冒泡,如果该值为true,则deepPath将只包含目标节点
composed 是否触发shadow root之外的监听,默认fasle 同时求教 shadow root 在这里指的是?
*/

参考文档:

MDN Document.creatEvent()

MDN Event.initEvent()

MDN EventTarget.dispatchEvent()

MDN Event

本文涉及的知识点比较基础,且看且勿喷吧。

如有不正之处,感谢指出... 同时欢迎讨论交流

fastclick 源码注解及一些基础知识点的更多相关文章

  1. 【读fastclick源码有感】彻底解决tap“点透”,提升移动端点击响应速度

    申明!!!最后发现判断有误,各位读读就好,正在研究中.....尼玛水太深了 前言 近期使用tap事件为老夫带来了这样那样的问题,其中一个问题是解决了点透还需要将原来一个个click变为tap,这样的话 ...

  2. DispatcherServlet源码注解分析

    DispatcherServlet的介绍与工作流程 DispatcherServlet是SpringMVC的前端分发控制器,用于处理客户端请求,然后交给对应的handler进行处理,返回对应的模型和视 ...

  3. fastclick源码分析

    https://www.cnblogs.com/diver-blogs/p/5657323.html  地址 fastclick.js源码解读分析 阅读优秀的js插件和库源码,可以加深我们对web开发 ...

  4. 移动端触摸、点击事件优化(fastclick源码学习)

    移动端触摸.点击事件优化(fastclick源码学习) 最近在做一些微信移动端的页面,在此记录关于移动端触摸和点击事件的学习优化过程,主要内容围绕fastclick展开.fastclick githu ...

  5. 【一起学源码-微服务】Hystrix 源码一:Hystrix基础原理与Demo搭建

    说明 原创不易,如若转载 请标明来源! 欢迎关注本人微信公众号:壹枝花算不算浪漫 更多内容也可查看本人博客:一枝花算不算浪漫 前言 前情回顾 上一个系列文章讲解了Feign的源码,主要是Feign动态 ...

  6. Jquery源码中的Javascript基础知识(三)

    这篇主要说一下在源码中jquery对象是怎样设计实现的,下面是相关代码的简化版本: (function( window, undefined ) { // code 定义变量 jQuery = fun ...

  7. 【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

    写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. vuejs是一个优秀的前端mvvm框架,它的易用性和渐进式的理念可以使每一个前端开发人员感到舒服,感到easy.它内 ...

  8. js调试系列: 源码定位与调试[基础篇]

    js调试系列目录: - 如果看了1, 2两篇,你对控制台应该有一个初步了解了,今天我们来个简单的调试.昨天留的三个课后练习,差不多就是今天要讲的内容.我们先来处理第一个问题:1. 查看文章下方 推荐 ...

  9. Vue.js 源码分析(十二) 基础篇 组件详解

    组件是可复用的Vue实例,一个组件本质上是一个拥有预定义选项的一个Vue实例,组件和组件之间通过一些属性进行联系. 组件有两种注册方式,分别是全局注册和局部注册,前者通过Vue.component() ...

随机推荐

  1. jquery手风琴

    --js $(document).ready(function(){ //Set default open/close settings$('.acc_container').hide(); //Hi ...

  2. jQuery选择什么版本 1.x? 2.x? 3.x?

    类似标题:jQuery选择什么版本?jquery一般用什么版本?jquery ie8兼容版本.jquery什么版本稳定? 目前jQuery有三个大版本:1.x:兼容ie678,使用最为广泛的,官方只做 ...

  3. MarkdownPad 2 常用快捷键

    Ctrl + I : 斜体 Ctrl + B : 粗体 Ctrl + G : 图片 Ctrl + Q : 引用 Ctrl + 1 : 标题 1 Ctrl + 2 : 标题 2 Ctrl + 3 : 标 ...

  4. IOS的七种手势

    今天为大家介绍一下IOS 的七种手势,手势在开发中经常用到,所以就简单 通俗易懂的说下, 话不多说,直接看代码: // 初始化一个UIimageView UIImageView *imageView ...

  5. 安装redis以windows服务形式

    安装redis以windows服务形式 安装redis以windows服务形式 redis windows windows 服务 以前跑redis,老是要开一个命令行窗口,一旦关闭,redis服务就挂 ...

  6. HDOJ 2561. 第二小整数 第k大问题

    第二小整数 Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Subm ...

  7. Intellij IDEA 一些不为人知的技巧

    Intellij IDEA 一些不为人知的技巧 2016/12/06 | 分类: 基础技术 | 0 条评论 | 标签: IntelliJ 分享到:38 原文出处: khotyn 今天又听了 Jetbr ...

  8. Linux下的串口编程及非阻塞模式

    本篇介绍了如何在linux系统下向串口发送数据.包括read的阻塞和非阻塞.以及select方法. 打开串口 在Linux系统下,打开串口是通过使用标准的文件打开函数操作的. #include < ...

  9. C#中ToString()格式详解

    以下内容均摘自博客园,仅供资料查询. ToString格式化 在很多对象显示为字符串的时候都会使用到ToString中的格式化,由于以前没怎么注意到这个问题,想总结一下各个基础结构对象的格式化,以便后 ...

  10. 纯css3图片旋转展示

    <!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>& ...