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

问题产生的原因

移动端浏览器的点击事件存在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. 【HTML5&CSS3进阶04】CSS3动画应该如何在webapp中运用

    动画在webapp的现状 webapp模式的网站追求的就是一个体验,是HTML5&CSS3浪潮下的产物,抛开体验不说,webapp模式门槛比较高: 而体验优化的一个重点便是动画,可以说动画是w ...

  2. 正确制作一个iframe,认识iframe

    iframe作为一个网站之间交互的桥梁,受到很多站长的喜爱,但是又有不安全的因素存在,所以正确填写属性是很重要的. <iframe name="my_iframe" heig ...

  3. 如何利用FineBI做财务分析

    很多企业随着业务规模的增长,传统的财务分析方式采用手工摘取数据的方式,难以快速地对企财务经营状况作出及时分析和预测.现在业务人员通过使用自助式BI工具做财务分析已经成为流行,每个人都希望自己做报表,快 ...

  4. Android中使用ListView实现分页刷新(线程休眠模拟)

    当要显示的数据过多时,为了更好的提升用户感知,在很多APP中都会使用分页刷新显示,比如浏览新闻,向下滑动到当前ListView的最后一条信息(item)时,会提示刷新加载,然后加载更新后的内容.此过程 ...

  5. 批处理bat 命令

    1.批处理常用符号: - echo 打开回显或关闭请求回显功能,或显示消息.如果没有任何参数,echo 命令将显示当前回显设置 语法:@echo [{ on|off }]  echo{"显示 ...

  6. 几款Git GUI客户端工具

    工欲善其事,必先利其器. 作为一名开发人员,你不可能不知道git,无论你是开发自己的开源项目还是和团队一起进行大规模产品的开发,git都已经是源代码管理工具的首选.当然,那些hardcore deve ...

  7. AngularJS 模块& 表单

    模块定义了一个应用程序. 模块是应用程序中不同部分的容器. 模块是应用控制器的容器. 控制器通常属于一个模块. 应用("myApp") 带有控制器 ("myCtrl&qu ...

  8. nfs挂载配置

    nfs挂载步骤 服务器端 1.安装nfs-utils rpcbind $sudo yum –y install nfs-utils rpcbind 2.文件开放出去配置/etc/exports 例子: ...

  9. oracel数据导出导入

    一.导出模式(三种模式)及命令格式 1. 全库模式 exp 用户名/密码@网络服务名 full=y file=路径\文件名.dmp log=路径\文件名.log 2. 用户模式(一般情况下采用此模式) ...

  10. shell 脚本之循环使用 for while 详解

    任何一种编程语言中循环是比不可少的,当然 shell 脚本也少不了循环语句,包括 for 语句. while 语句.文中主要以实际用例来说明 for while 都有哪些常见的使用方法和技巧. 一.f ...