FastClick 填坑及源码解析
最近产品妹子提出了一个体验issue —— 用 iOS 在手Q阅读书友交流区发表书评时,光标点击总是不好定位到正确的位置:
如上图,具体表现是较快点击时,光标总会跳到 textarea 内容的尾部。只有当点击停留时间较久一点(比如超过150ms)才能把光标正常定位到正确的位置。
一开始我以为是 iOS 原生的交互问题没太在意,但后来发现访问某些页面又是没有这种奇怪体验的。
然后怀疑是否 JS 注册了某些事件导致的问题,于是试着把业务模块移除了再跑一遍,发现问题照旧。
于是只好继续做排除法,把页面上的一些库一点点移掉再运行页面,结果发现捣乱的小鬼果然是嫌疑最大的 Fastclick。
然后呢,我试着按API所说,给 textarea 加上一个名为“needsclick”的类名,希望能绕过 fastclick 的处理直接走原生点击事件,结果讶异地发现屁用没有。。。
对此感谢后面我们小组的 kindeng 童鞋帮忙研究了下并提供了解决方案,不过我还想进一步研究到底是什么原因导致了这个坑、Fastclick 对我的页面做了神马~
所以昨晚花了点时间一口气把源码都蹂躏了一遍。
这会是一篇很长的文章,但会是注释非常详尽的剖析文。
文章带分析的源码我也挂在我的 github 仓库上了,有兴趣的童鞋可以去下载来看。
闲话不多说,咱们开始深入 FastClick 源码阵营。
我们知道,注册一个 FastClick 事件非常简单,它是这样的:
- if ('addEventListener' in document) {
- document.addEventListener('DOMContentLoaded', function() {
- var fc = FastClick.attach(document.body); //生成实例
- }, false);
- }
所以我们从这里着手,打开源码看下 FastClick .attach 方法:
- FastClick.attach = function(layer, options) {
- return new FastClick(layer, options);
- };
这里返回了一个 FastClick 实例,所以咱们拉到前面看看 FastClick 构造函数:
- function FastClick(layer, options) {
- var oldOnClick;
- options = options || {};
- //定义了一些参数...
- //如果是属于不需要处理的元素类型,则直接返回
- if (FastClick.notNeeded(layer)) {
- return;
- }
- //语法糖,兼容一些用不了 Function.prototype.bind 的旧安卓
- //所以后面不走 layer.addEventListener('click', this.onClick.bind(this), true);
- function bind(method, context) {
- return function() { return method.apply(context, arguments); };
- }
- var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
- var context = this;
- for (var i = 0, l = methods.length; i < l; i++) {
- context[methods[i]] = bind(context[methods[i]], context);
- }
- //安卓则做额外处理
- 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);
- // 兼容不支持 stopImmediatePropagation 的浏览器(比如 Android 2)
- if (!Event.prototype.stopImmediatePropagation) {
- layer.removeEventListener = function(type, callback, capture) {
- var rmv = Node.prototype.removeEventListener;
- if (type === 'click') {
- rmv.call(layer, type, callback.hijacked || callback, capture);
- } else {
- rmv.call(layer, type, callback, capture);
- }
- };
- layer.addEventListener = function(type, callback, capture) {
- var adv = Node.prototype.addEventListener;
- if (type === 'click') {
- //留意这里 callback.hijacked 中会判断 event.propagationStopped 是否为真来确保(安卓的onMouse事件)只执行一次
- //在 onMouse 事件里会给 event.propagationStopped 赋值 true
- adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
- if (!event.propagationStopped) {
- callback(event);
- }
- }), capture);
- } else {
- adv.call(layer, type, callback, capture);
- }
- };
- }
- // 如果layer直接在DOM上写了 onclick 方法,那我们需要把它替换为 addEventListener 绑定形式
- if (typeof layer.onclick === 'function') {
- oldOnClick = layer.onclick;
- layer.addEventListener('click', function(event) {
- oldOnClick(event);
- }, false);
- layer.onclick = null;
- }
- }
在初始通过 FastClick.notNeeded 方法判断是否需要做后续的相关处理:
- //如果是属于不需要处理的元素类型,则直接返回
- if (FastClick.notNeeded(layer)) {
- return;
- }
我们看下这个 FastClick.notNeeded 都做了哪些判断:
- //是否没必要使用到 Fastclick 的检测
- FastClick.notNeeded = function(layer) {
- var metaViewport;
- var chromeVersion;
- var blackberryVersion;
- var firefoxVersion;
- // 不支持触摸的设备
- if (typeof window.ontouchstart === 'undefined') {
- return true;
- }
- // 获取Chrome版本号,若非Chrome则返回0
- chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
- if (chromeVersion) {
- if (deviceIsAndroid) { //安卓
- metaViewport = document.querySelector('meta[name=viewport]');
- if (metaViewport) {
- // 安卓下,带有 user-scalable="no" 的 meta 标签的 chrome 是会自动禁用 300ms 延迟的,所以无需 Fastclick
- if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
- return true;
- }
- // 安卓Chrome 32 及以上版本,若带有 width=device-width 的 meta 标签也是无需 FastClick 的
- if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
- return true;
- }
- }
- // 其它的就肯定是桌面级的 Chrome 了,更不需要 FastClick 啦
- } else {
- return true;
- }
- }
- if (deviceIsBlackBerry10) { //黑莓,和上面安卓同理,就不写注释了
- blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);
- if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
- metaViewport = document.querySelector('meta[name=viewport]');
- if (metaViewport) {
- if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
- return true;
- }
- if (document.documentElement.scrollWidth <= window.outerWidth) {
- return true;
- }
- }
- }
- }
- // 带有 -ms-touch-action: none / manipulation 特性的 IE10 会禁用双击放大,也没有 300ms 时延
- if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {
- return true;
- }
- // Firefox检测,同上
- firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
- if (firefoxVersion >= 27) {
- metaViewport = document.querySelector('meta[name=viewport]');
- if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {
- return true;
- }
- }
- // IE11 推荐使用没有“-ms-”前缀的 touch-action 样式特性名
- if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {
- return true;
- }
- return false;
- };
基本上都是一些能禁用 300ms 时延的浏览器嗅探,它们都没必要使用 Fastclick,所以会返回 true 回构造函数停止下一步执行。
由于安卓手Q的 ua 会被匹配到 /Chrome\/([0-9]+)/,故带有 'user-scalable=no' meta 标签的安卓手Q页会被 FastClick 视为无需处理页。
这也是为何在安卓手Q里没有开头提及问题的原因。
我们继续看构造函数,它直接给 layer(即body)添加了click、touchstart、touchmove、touchend、touchcancel(若是安卓还有 mouseover、mousedown、mouseup)事件监听:
- //安卓则做额外处理
- 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);
注意在这段代码上面还利用了 bind 方法做了处理,这些事件回调中的 this 都会变成 Fastclick 实例上下文。
另外还得留意,onclick 事件以及安卓的额外处理部分都是走的捕获监听。
咱们分别看看这些事件回调分别都做了什么。
1. this.onTouchStart
- FastClick.prototype.onTouchStart = function(event) {
- var targetElement, touch, selection;
- // 多指触控的手势则忽略
- if (event.targetTouches.length > 1) {
- return true;
- }
- targetElement = this.getTargetElementFromEventTarget(event.target); //一些较老的浏览器,target 可能会是一个文本节点,得返回其DOM节点
- touch = event.targetTouches[0];
- if (deviceIsIOS) { //IOS处理
- // 若用户已经选中了一些内容(比如选中了一段文本打算复制),则忽略
- selection = window.getSelection();
- if (selection.rangeCount && !selection.isCollapsed) {
- return true;
- }
- if (!deviceIsIOS4) { //是否IOS4
- //怪异特性处理——若click事件回调打开了一个alert/confirm,用户下一次tap页面的其它地方时,新的touchstart和touchend
- //事件会拥有同一个touch.identifier(新的 touch event 会跟上一次触发alert点击的 touch event 一样),
- //为避免将新的event当作之前的event导致问题,这里需要禁用事件
- //另外chrome的开发工具启用'Emulate touch events'后,iOS UA下的 identifier 会变成0,所以要做容错避免调试过程也被禁用事件了
- if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
- event.preventDefault();
- return false;
- }
- this.lastTouchIdentifier = touch.identifier;
- // 如果target是一个滚动容器里的一个子元素(使用了 -webkit-overflow-scrolling: touch) ,而且满足:
- // 1) 用户非常快速地滚动外层滚动容器
- // 2) 用户通过tap停止住了这个快速滚动
- // 这时候最后的'touchend'的event.target会变成用户最终手指下的那个元素
- // 所以当快速滚动开始的时候,需要做检查target是否滚动容器的子元素,如果是,做个标记
- // 在touchend时检查这个标记的值(滚动容器的scrolltop)是否改变了,如果是则说明页面在滚动中,需要取消fastclick处理
- this.updateScrollParent(targetElement);
- }
- }
- this.trackingClick = true; //做个标志表示开始追踪click事件了
- this.trackingClickStart = event.timeStamp; //标记下touch事件开始的时间戳
- this.targetElement = targetElement;
- //标记touch起始点的页面偏移值
- this.touchStartX = touch.pageX;
- this.touchStartY = touch.pageY;
- // this.lastClickTime 是在 touchend 里标记的事件时间戳
- // this.tapDelay 为常量 200 (ms)
- // 此举用来避免 phantom 的双击(200ms内快速点了两次)触发 click
- // 反正200ms内的第二次点击会禁止触发其默认事件
- if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
- event.preventDefault();
- }
- return true;
- };
顺道看下这里的 this.updateScrollParent:
- /**
- * 检查target是否一个滚动容器里的子元素,如果是则给它加个标记
- */
- FastClick.prototype.updateScrollParent = function(targetElement) {
- var scrollParent, parentElement;
- scrollParent = targetElement.fastClickScrollParent;
- if (!scrollParent || !scrollParent.contains(targetElement)) {
- parentElement = targetElement;
- do {
- if (parentElement.scrollHeight > parentElement.offsetHeight) {
- scrollParent = parentElement;
- targetElement.fastClickScrollParent = parentElement;
- break;
- }
- parentElement = parentElement.parentElement;
- } while (parentElement);
- }
- // 给滚动容器加个标志fastClickLastScrollTop,值为其当前垂直滚动偏移
- if (scrollParent) {
- scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
- }
- };
另外要注意的是,在 onTouchStart 里被标记为 true 的 this.trackingClick 属性,都会在其它事件回调(比如 ontouchmove )的开头做检测,如果没被赋值过,则直接忽略:
- if (!this.trackingClick) {
- return true;
- }
当然在 ontouchend 事件里会把它重置为 false。
2. this.onTouchMove
这段代码量好少:
- FastClick.prototype.onTouchMove = function(event) {
- //不是需要被追踪click的事件则忽略
- if (!this.trackingClick) {
- return true;
- }
- // 如果target突然改变了,或者用户其实是在移动手势而非想要click
- // 则应该清掉this.trackingClick和this.targetElement,告诉后面的事件你们也不用处理了
- if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
- this.trackingClick = false;
- this.targetElement = null;
- }
- return true;
- };
看下这里用到的 this.touchHasMoved 原型方法:
- //判断是否移动了
- //this.touchBoundary是常量,值为10
- //如果touch已经移动了10个偏移量单位,则应当作为移动事件处理而非click事件
- FastClick.prototype.touchHasMoved = function(event) {
- var touch = event.changedTouches[0], boundary = this.touchBoundary;
- if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
- return true;
- }
- return false;
- };
3. onTouchEnd
- FastClick.prototype.onTouchEnd = function(event) {
- var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
- if (!this.trackingClick) {
- return true;
- }
- // 避免 phantom 的双击(200ms内快速点了两次)触发 click
- // 我们在 ontouchstart 里已经做过一次判断了(仅仅禁用默认事件),这里再做一次判断
- if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
- this.cancelNextClick = true; //该属性会在 onMouse 事件中被判断,为true则彻底禁用事件和冒泡
- return true;
- }
- //this.tapTimeout是常量,值为700
- //识别是否为长按事件,如果是(大于700ms)则忽略
- if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
- return true;
- }
- // 得重置为false,避免input事件被意外取消
- // 例子见 https://github.com/ftlabs/fastclick/issues/156
- this.cancelNextClick = false;
- this.lastClickTime = event.timeStamp; //标记touchend时间,方便下一次的touchstart做双击校验
- trackingClickStart = this.trackingClickStart;
- //重置 this.trackingClick 和 this.trackingClickStart
- this.trackingClick = false;
- this.trackingClickStart = 0;
- // iOS 6.0-7.*版本下有个问题 —— 如果layer处于transition或scroll过程,event所提供的target是不正确的
- // 所以咱们得重找 targetElement(这里通过 document.elementFromPoint 接口来寻找)
- if (deviceIsIOSWithBadTarget) { //iOS 6.0-7.*版本
- touch = event.changedTouches[0]; //手指离开前的触点
- // 有些情况下 elementFromPoint 里的参数是预期外/不可用的, 所以还得避免 targetElement 为 null
- targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
- // target可能不正确需要重找,但fastClickScrollParent是不会变的
- targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
- }
- targetTagName = targetElement.tagName.toLowerCase();
- if (targetTagName === 'label') { //是label则激活其指向的组件
- forElement = this.findControl(targetElement);
- if (forElement) {
- this.focus(targetElement);
- //安卓直接返回(无需合成click事件触发,因为点击和激活元素不同,不存在点透)
- if (deviceIsAndroid) {
- return false;
- }
- targetElement = forElement;
- }
- } else if (this.needsFocus(targetElement)) { //非label则识别是否需要focus的元素
- //手势停留在组件元素时长超过100ms,则置空this.targetElement并返回
- //(而不是通过调用this.focus来触发其聚焦事件,走的原生的click/focus事件触发流程)
- //这也是为何文章开头提到的问题中,稍微久按一点(超过100ms)textarea是可以把光标定位在正确的地方的原因
- //另外iOS下有个意料之外的bug——如果被点击的元素所在文档是在iframe中的,手动调用其focus的话,
- //会发现你往其中输入的text是看不到的(即使value做了更新),so这里也直接返回
- if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
- this.targetElement = null;
- return false;
- }
- this.focus(targetElement);
- this.sendClick(targetElement, event); //立即触发其click事件,而无须等待300ms
- //iOS4下的 select 元素不能禁用默认事件(要确保它能被穿透),否则不会打开select目录
- //有时候 iOS6/7 下(VoiceOver开启的情况下)也会如此
- if (!deviceIsIOS || targetTagName !== 'select') {
- this.targetElement = null;
- event.preventDefault();
- }
- return false;
- }
- if (deviceIsIOS && !deviceIsIOS4) {
- // 滚动容器的垂直滚动偏移改变了,说明是容器在做滚动而非点击,则忽略
- scrollParent = targetElement.fastClickScrollParent;
- if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
- return true;
- }
- }
- // 查看元素是否无需处理的白名单内(比如加了名为“needsclick”的class)
- // 不是白名单的则照旧预防穿透处理,立即触发合成的click事件
- if (!this.needsClick(targetElement)) {
- event.preventDefault();
- this.sendClick(targetElement, event);
- }
- return false;
- };
这段比较长,我们主要看这段:
- } else if (this.needsFocus(targetElement)) { //非label则识别是否需要focus的元素
- //手势停留在组件元素时长超过100ms,则置空this.targetElement并返回
- //(而不是通过调用this.focus来触发其聚焦事件,走的原生的click/focus事件触发流程)
- //这也是为何文章开头提到的问题中,稍微久按一点(超过100ms)textarea是可以把光标定位在正确的地方的原因
- //另外iOS下有个意料之外的bug——如果被点击的元素所在文档是在iframe中的,手动调用其focus的话,
- //会发现你往其中输入的text是看不到的(即使value做了更新),so这里也直接返回
- if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
- this.targetElement = null;
- return false;
- }
- this.focus(targetElement);
- this.sendClick(targetElement, event); //立即触发其click事件,而无须等待300ms
- //iOS4下的 select 元素不能禁用默认事件(要确保它能被穿透),否则不会打开select目录
- //有时候 iOS6/7 下(VoiceOver开启的情况下)也会如此
- if (!deviceIsIOS || targetTagName !== 'select') {
- this.targetElement = null;
- event.preventDefault();
- }
- return false;
- }
其中 this.needsFocus 用于判断给定元素是否需要通过合成click事件来模拟聚焦:
- //判断给定元素是否需要通过合成click事件来模拟聚焦
- FastClick.prototype.needsFocus = function(target) {
- switch (target.nodeName.toLowerCase()) {
- case 'textarea':
- return true;
- case 'select':
- return !deviceIsAndroid; //iOS下的select得走穿透点击才行
- case 'input':
- switch (target.type) {
- case 'button':
- case 'checkbox':
- case 'file':
- case 'image':
- case 'radio':
- case 'submit':
- return false;
- }
- return !target.disabled && !target.readOnly;
- default:
- //带有名为“bneedsfocus”的class则返回true
- return (/\bneedsfocus\b/).test(target.className);
- }
- };
另外这段说明了为何稍微久按一点(超过100ms)textarea ,我们是可以把光标定位在正确的地方(会绕过后面调用 this.focus 的方法):
- //手势停留在组件元素时长超过100ms,则置空this.targetElement并返回
- //(而不是通过调用this.focus来触发其聚焦事件,走的原生的click/focus事件触发流程)
- //这也是为何文章开头提到的问题中,稍微久按一点(超过100ms)textarea是可以把光标定位在正确的地方的原因
- //另外iOS下有个意料之外的bug——如果被点击的元素所在文档是在iframe中的,手动调用其focus的话,
- //会发现你往其中输入的text是看不到的(即使value做了更新),so这里也直接返回
- if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
- this.targetElement = null;
- return false;
- }
接着咱们看看这两行很重要的代码:
- this.focus(targetElement);
- this.sendClick(targetElement, event); //立即触发其click事件,而无须等待300ms
所涉及的两个原型方法分别为:
⑴ this.focus
- FastClick.prototype.focus = function(targetElement) {
- var length;
- // 组件建议通过setSelectionRange(selectionStart, selectionEnd)来设定光标范围(注意这样还没有聚焦
- // 要等到后面触发 sendClick 事件才会聚焦)
- // 另外 iOS7 下有些input元素(比如 date datetime month) 的 selectionStart 和 selectionEnd 特性是没有整型值的,
- // 导致会抛出一个关于 setSelectionRange 的模糊错误,它们需要改用 focus 事件触发
- if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
- length = targetElement.value.length;
- targetElement.setSelectionRange(length, length);
- } else {
- //直接触发其focus事件
- targetElement.focus();
- }
- };
注意,我们点击 textarea 时调用了该方法,它通过 targetElement.setSelectionRange(length, length) 决定了光标的位置在内容的尾部(但注意,这时候还没聚焦!!!)。
⑵ this.sendClick
真正让 textarea 聚焦的是这个方法,它合成了一个 click 方法立刻在textarea元素上触发导致聚焦:
- //合成一个click事件并在指定元素上触发
- FastClick.prototype.sendClick = function(targetElement, event) {
- var clickEvent, touch;
- // 在一些安卓机器中,得让页面所存在的 activeElement(聚焦的元素,比如input)失焦,否则合成的click事件将无效
- if (document.activeElement && document.activeElement !== targetElement) {
- document.activeElement.blur();
- }
- touch = event.changedTouches[0];
- // 合成(Synthesise) 一个 click 事件
- // 通过一个额外属性确保它能被追踪(tracked)
- 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; // fastclick的内部变量,用来识别click事件是原生还是合成的
- targetElement.dispatchEvent(clickEvent); //立即触发其click事件
- };
- FastClick.prototype.determineEventType = function(targetElement) {
- //安卓设备下 Select 无法通过合成的 click 事件被展开,得改为 mousedown
- if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
- return 'mousedown';
- }
- return 'click';
- };
经过这么一折腾,咱们轻点 textarea 后,光标就自然定位到其内容尾部去了。但是这里有个问题——排在 touchend 后的 focus 事件为啥没被触发呢?
如果 focus 事件能被触发的话,那肯定能重新定位光标到正确的位置呀。
咱们看下面这段:
- //iOS4下的 select 元素不能禁用默认事件(要确保它能被穿透),否则不会打开select目录
- //有时候 iOS6/7 下(VoiceOver开启的情况下)也会如此
- if (!deviceIsIOS || targetTagName !== 'select' ) {
- this.targetElement = null;
- event.preventDefault();
- }
通过 preventDefault 的阻挡,textarea 自然再也无法拥抱其 focus 宝宝了~
于是乎,我们在这里做个改动就能修复这个问题:
- var _isTextInput = function(){
- return targetTagName === 'textarea' || (targetTagName === 'input' && targetElement.type === 'text');
- };
- if ((!deviceIsIOS || targetTagName !== 'select') && !_isTextInput()) {
- this.targetElement = null;
- event.preventDefault();
- }
或者:
- if (!deviceIsIOS4 || targetTagName !== 'select') {
- this.targetElement = null;
- //给textarea加上“needsclick”的class
- if((!/\bneedsclick\b/).test(targetElement.className)){
- event.preventDefault();
- }
- }
这里要吐槽下的是,Fastclick 把 this.needsClick 放到了 ontouchEnd 末尾去执行,才导致前面说的加上了“needsclick”类名也无效的问题。
虽然问题原因找到也解决了,但咱们还是继续看剩下的部分吧。
4. onMouse 和 onClick
- //用于决定是否允许穿透事件(触发layer的click默认事件)
- FastClick.prototype.onMouse = function(event) {
- // touch事件一直没触发
- if (!this.targetElement) {
- return true;
- }
- if (event.forwardedTouchEvent) { //触发的click事件是合成的
- return true;
- }
- // 编程派生的事件所对应元素事件可以被允许
- // 确保其没执行过 preventDefault 方法(event.cancelable 不为 true)即可
- if (!event.cancelable) {
- return true;
- }
- // 需要做预防穿透处理的元素,或者做了快速(200ms)双击的情况
- if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
- //停止当前默认事件和冒泡
- if (event.stopImmediatePropagation) {
- event.stopImmediatePropagation();
- } else {
- // 不支持 stopImmediatePropagation 的设备(比如Android 2)做标记,
- // 确保该事件回调不会执行(见126行)
- event.propagationStopped = true;
- }
- // 取消事件和冒泡
- event.stopPropagation();
- event.preventDefault();
- return false;
- }
- //允许穿透
- return true;
- };
- //click事件常规都是touch事件衍生来的,也排在touch后面触发。
- //对于那些我们在touch事件过程没有禁用掉默认事件的event来说,我们还需要在click的捕获阶段进一步
- //做判断决定是否要禁掉点击事件(防穿透)
- FastClick.prototype.onClick = function(event) {
- var permitted;
- // 如果还有 trackingClick 存在,可能是某些UI事件阻塞了touchEnd 的执行
- if (this.trackingClick) {
- this.targetElement = null;
- this.trackingClick = false;
- return true;
- }
- // 依旧是对 iOS 怪异行为的处理 —— 如果用户点击了iOS模拟器里某个表单中的一个submit元素
- // 或者点击了弹出来的键盘里的“Go”按钮,会触发一个“伪”click事件(target是一个submit-type的input元素)
- if (event.target.type === 'submit' && event.detail === 0) {
- return true;
- }
- permitted = this.onMouse(event);
- if (!permitted) { //如果点击是被允许的,将this.targetElement置空可以确保onMouse事件里不会阻止默认事件
- this.targetElement = null;
- }
- //没有多大意义
- return permitted;
- };
- //销毁Fastclick所注册的监听事件。是给外部实例去调用的
- FastClick.prototype.destroy = function() {
- var layer = this.layer;
- if (deviceIsAndroid) {
- layer.removeEventListener('mouseover', this.onMouse, true);
- layer.removeEventListener('mousedown', this.onMouse, true);
- layer.removeEventListener('mouseup', this.onMouse, true);
- }
- layer.removeEventListener('click', this.onClick, true);
- layer.removeEventListener('touchstart', this.onTouchStart, false);
- layer.removeEventListener('touchmove', this.onTouchMove, false);
- layer.removeEventListener('touchend', this.onTouchEnd, false);
- layer.removeEventListener('touchcancel', this.onTouchCancel, false);
- };
常规需要阻断点击事件的操作,我们在 touch 监听事件回调中已经做了处理,这里主要是针对那些 touch 过程(有些设备甚至可能并没有touch事件触发)没有禁用默认事件的 event 做进一步处理,从而决定是否触发原生的 click 事件(如果禁止是在 onMouse 方法里做的处理)。
小结
1. 在 fastclick 源码的 addEventListener 回调事件中有很多的 return false/true。它们其实主要用于绕过后面的脚本逻辑,并没有其它意义(它是不会阻止默认事件的)。
所以千万别把 jQuery 事件、或者 DOM0 级事件回调中的 return false 概念,跟 addEventListener 的混在一起了。
2. fastclick 的源码其实很简单,有很大部分不外乎对一些怪异行为做 hack,其核心理念不外乎是——捕获 target 事件,判断 target 是要解决点透问题的元素,就合成一个 click 事件在 target 上触发,同时通过 preventDefault 禁用默认事件。
3. fastclick 虽好,但也有一些坑,还是得按需求对其修改,那么了解其源码还是很有必要的。
FastClick 填坑及源码解析的更多相关文章
- 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...
- Exynos 4412 Uboot源码解析
原文地址:http://www.cnblogs.com/jacklu/p/6226330.html Exynos 4412 Uboot的汇编代码就不贴了,没有的可以私信我. 这是我当时阅读代码时的思维 ...
- 神经网络中 BP 算法的原理与 Python 实现源码解析
最近这段时间系统性的学习了 BP 算法后写下了这篇学习笔记,因为能力有限,若有明显错误,还请指正. 什么是梯度下降和链式求导法则 假设我们有一个函数 J(w),如下图所示. 梯度下降示意图 现在,我们 ...
- 黄聪:WordPress动作钩子函数add_action()、do_action()源码解析
WordPress常用两种钩子,过滤钩子和动作钩子.过滤钩子相关函数及源码分析在上篇文章中完成,本篇主要分析动作钩子源码. 然而,在了解了动作钩子的源码后你会发现,动作钩子核心代码竟然跟过滤钩子差不多 ...
- 给jdk写注释系列之jdk1.6容器(12)-PriorityQueue源码解析
PriorityQueue是一种什么样的容器呢?看过前面的几个jdk容器分析的话,看到Queue这个单词你一定会,哦~这是一种队列.是的,PriorityQueue是一种队列,但是它又是一种什么样的队 ...
- 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析
前面了解了jdk容器中的两种List,回忆一下怎么从list中取值(也就是做查询),是通过index索引位置对不对,由于存入list的元素时安装插入顺序存储的,所以index索引也就是插入的次序. M ...
- ArrayList源码解析
ArrayList简介 ArrayList定义 1 public class ArrayList<E> extends AbstractList<E> implements L ...
- 2015.07.20MapReducer源码解析(笔记)
MapReducer源码解析(笔记) 第一步,读取数据源,将每一行内容解析成一个个键值对,每个键值对供map函数定义一次,数据源由FileInputFormat:指定的,程序就能从地址读取记录,读 ...
- anki_vector SDK源码解析(教程)
一:最近anki vector robot开放了Python SDK,我听到的第一时间就赶快上网查了查,先抛几个官网重要链接吧: Python编程API手册及环境搭建等: https://sdk-re ...
随机推荐
- Vue + Webpack + Vue-loader 系列教程(1)功能介绍篇
原文地址:https://lvyongbo.gitbooks.io/vue-loader/content/ Vue-loader 是什么? vue-loader 是一个加载器,能把如下格式的 Vue ...
- WebApi接口 - 响应输出xml和json
格式化数据这东西,主要看需要的运用场景,今天和大家分享的是webapi格式化数据,这里面的例子主要是输出json和xml的格式数据,测试用例很接近实际常用情况:希望大家喜欢,也希望各位多多扫码支持和点 ...
- HTML 5 应用程序缓存manifest
什么是应用程序缓存(Application Cache)? HTML5 引入了应用程序缓存,这意味着 web 应用可进行缓存,并可在没有因特网连接时进行访问. 应用程序缓存为应用带来三个优势: 离线浏 ...
- Android之文件数据存储
一.文件保存数据介绍 Activity提供了openFileOutput()方法可以用于把数据输出到文件中,具体的实现过程与在J2SE环境中保存数据到文件中是一样的.文件可用来存放大量数据,如文本.图 ...
- iOS之ProtocolBuffer搭建和示例demo
这次搭建iOS的ProtocolBuffer编译器和把*.proto源文件编译成*.pbobjc.h 和 *.pbobjc.m文件时,碰到不少问题! 搭建pb编译器到时没有什么问题,只是在把*.pro ...
- maven 快照
大型应用软件一般由多个模块组成,一般它是多个团队开发同一个应用程序的不同模块,这是比较常见的场景.例如,一个团队正在对应用程序的应用程序,用户界面项目(app-ui.jar:1.0) 的前端进行开发, ...
- 完美解决CodeSmith无法获取MySQL表及列Description说明注释的方案
问题描述: CodeSmith是现在比较实用的代码生成器,但是我们发现一个问题: 使用CodeSmith编写MySQL模板的时候,会发现一个问题:MySQL数据表中的列说明获取不到,也就是column ...
- Mysql - 函数
Mysql提供的函数是在是太多了, 很多我都见过, 别说用了. 园子里面, 有人弄了一个比较全的. MYSQL函数 我这里会将他写的完全拷贝下来, 中间会插入一些自己项目中使用过的心得 一.数学函数 ...
- vs生成pro
1.修改.vcxproj文件 <PropertyGroup Label="Globals"> <ProjectGuid>{AAAA4039-13B ...
- C 盘的不速之客
C 盘的报告内容既然上GB的空间 操作系统版本 原来是微软这个查找解决异常关闭解决方案生成的报告 参考 How To Disable Vista Error Reporting Feature ...