真的了解FastClick吗?

六眼飞鱼酱① 提交于 2020-02-18 21:25:17

真的了解FastClick吗?:https://www.cnblogs.com/ylweb/p/10549040.html

 

你真的了解FastClick吗?

前段时间在做公司官网手机端菜单部分的时候,遇到一些很诡异的点击问题。比如菜单点击无效/双击才有效、在手指滑动的时候会触发点击事件、以及同样的事件处理在微信跟浏览器会有不一样的表现等等,这些问题我一直试图用一些移动端事件的hack来解决,到最后还是有两个问题没有解决掉。后来意识到可能是引入的插件导致的事件冲突引起,因为一直都在全局引入了fastclick,以及最初偷懒引入的一个菜单功能插件(插件中有引入iScroll)。经过排查最后得出结论是fastclick与插件 冲突所致,只能去除插件重写菜单功能。而这个小插曲也让我有兴趣阅读一下它的源码来深究一下fastclick到底做了什么?

FastClick的使用场景及背景:

  • 移动端的开发经常需要监听用户的双击行为,事件的发生顺序是这样的:touchstart---touchmove---touchend,然后大约过300ms触发click事件,用来判断是否有双击事件。
  • 在混合使用touch与click时,会导致点击穿透!(此处不展开讨论)
  • FastClick的思路就是利用touch来模拟tap(触碰),如果认为是一次有效的tap,则在touchend时立即模拟一个click事件,分发到事件源(相当于主动触发一次click),同时阻止掉浏览器300ms后产生的click。自然也不存在点击穿透的问题。

众所周知的FastClick用法:

 

Javascript原生

if ('addEventListener' in document) {    document.addEventListener('DOMContentLoaded', function() {        FastClick.attach(document.body);    }, false);}

 

jQuery

$(function() {    FastClick.attach(document.body);});

 

类似Common JS的模块系统方式

var attachFastClick = require('fastclick');attachFastClick(document.body);

 

needsclick

对于页面上不需要使用fastclick来立刻触发点击事件的元素在元素标签的class上添加needsclick

 

不需要使用fastclick的情况

  • PC端,FastClick只在移动端监听;

  • Android版Chrome 32+浏览器,如果设置viewport meta的值为width=device-width,这种情况下浏览器会马上出发点击事件,不会延迟300毫秒。

<meta name="viewport" content="width=device-width, initial-scale=1">

 

  • 所有版本的Android Chrome浏览器,如果设置viewport meta的值有user-scalable=no,浏览器也是会立即触发点击事件。
  • IE11+浏览器设置了css的属性touch-action: manipulation,它会在某些标签(a,button等)禁止双击事件,IE10的为-ms-touch-action: manipulation

FastClick的实现原理

Fastclick的源码中除了对旧版本浏览器的polyfill以及特殊版本浏览器的的bug解决,主要绑定了以下原型方法:

/*构造函数*/function FastClick(layer, options)/*判断是否需要浏览器原生的click事件(针对一些特殊元素比如表单)*/FastClick.prototype.needsClick = function(target)/*判断给定元素是否需要通过合成click事件来模拟聚焦*/FastClick.prototype.needsFocus = function(target)/*合成click事件并在指定元素上触发*/FastClick.prototype.sendClick = function(targetElement, event)/* touchstart */FastClick.prototype.onTouchStart = function(event)/* touchmove*/FastClick.prototype.onTouchMove = function(event)/* touchend*/FastClick.prototype.onTouchEnd = function(event)/*判断这次tap是否有效*/FastClick.prototype.onMouse = function(event) /*click handler 捕获阶段监听*/FastClick.prototype.onClick = function(event)/*移出fastlick事件绑定*/FastClick.prototype.destroy = function()/*调用FastClick*/FastClick.attach = function(layer, options) {    return new FastClick(layer, options);};

先用一张图来解构FastClick源码(图片来自其他博客):

 

初始化的时候主要都做了什么呢?

/*不需要处理的元素类型,则直接返回(这些情况上面已经提到)*/if (FastClick.notNeeded(layer)) {       return;}/*Some old versions of Android don't have Function.prototype.bind*//*对安卓老版本不支持bind的polyfill*/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);}/* 如果layer直接在DOM上写了 onclick 方法,那我们需要把它替换为 addEventListener 绑定形式*/if (typeof layer.onclick === 'function') {    oldOnClick = layer.onclick;    layer.addEventListener('click', function(event) {        oldOnClick(event);    }, false);    layer.onclick = null;}

 

在FastClick.prototype.needsClick中有如下一行代码即是对 needsclick的判断:

 /*元素带了名为“needsclick”的class返回true*/ return (/\bneedsclick\b/).test(target.className);

 

在FastClick.prototype.needsFocus 中有如下一行代码即是对 needsfocus 的判断:

 /*带有名为“needsfocus”的class则返回true*/ return (/\bneedsfocus\b/).test(target.className);

 

touchstart事件

FastClick.prototype.onTouchStart = function(event) {    var targetElement, touch, selection;    /*多指触控手势则忽略*/    if (event.targetTouches.length > 1) {        return true;    }    /*某些旧浏览器,如果target是一个文本节点,得返回其DOM节点*/    targetElement = this.getTargetElementFromEventTarget(event.target);    touch = event.targetTouches[0];    if (deviceIsIOS) {        /*若用户已经选中了一些内容(比如选中了一段文本打算复制),则忽略*/        selection = window.getSelection();        if (selection.rangeCount && !selection.isCollapsed) {            return true;        }        if (!deviceIsIOS4) {             /*             怪异特性处理——若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 里标记的事件时间戳      his.tapDelay 为常量 200 (ms)      此举用来避免 phantom 的双击(200ms内快速点了两次)触发 click      反正200ms内的第二次点击会禁止触发点击的默认事件    */  if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {        event.preventDefault();    }    return true;};

 

touchmove事件

FastClick.prototype.onTouchMove = function(event) {    if (!this.trackingClick) {        return true;    }    /* If the touch has moved, cancel the click tracking*/    if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {        this.trackingClick = false;        this.targetElement = null;    }    return true;};

 

touchend事件

FastClick.prototype.onTouchEnd = function(event) {        var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;        if (!this.trackingClick) {            return true;        }        /*避免双击(200ms内快速点了两次)触发 click*/        if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {            /*该属性会在 onMouse 事件中被判断,为true则彻底禁用事件和冒泡*/            this.cancelNextClick = true;             return true;        }        /*识别是否为长按事件,如果是(大于700ms)则忽略*/        if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {            return true;        }        /*得重置为false,避免input事件被意外取消*/        this.cancelNextClick = false;        /*标记touchend时间,方便下一次的touchstart做双击校验*/        this.lastClickTime = event.timeStamp;        trackingClickStart = this.trackingClickStart;        /*重置 this.trackingClick 和 this.trackingClickStart*/        this.trackingClick = false;        this.trackingClickStart = 0;        /* iOS 6.0-7.*版本下有个问题 —— 如果layer处于transition或scroll过程,event所提供的target是不正确的*/        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事件触发流程)                 另外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);            /*立即触发其click事件*/            this.sendClick(targetElement, event);            /*      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;    };

 

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在IOS11.3以上版本的bug及解决方案

 

1:文本框在内容区点击不会立即定位到相应位置,而是在文本末尾。而长摁超过100ms则无此问题。

首先交互肯定是在focus的时候发生,让我们看下FastClick里的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();        }

在IOS下是通过targetElement.setSelectionRange来定位位置,至于在iOS11.3下为什么会出现这个bug,仍未知,解决的方法简单暴力,直接改写此方法:

FastClick.prototype.focus = function(targetElement) {    targetElement.focus();};

 

2:ios11.3支持了Web API:允许对事件支持被动模式,减少滚动屏幕的性能损耗和奔溃,并且针对document的touch事件监听添加被动模式的配置,因此document将不再调用preventDefault方法。这些改动会引起fastclick的另一个bug,当静置app或锁屏几秒后页面将无法响应任何点击操作。

解决方法:

layer.addEventListener('touchstart', this.onTouchStart, {passive:false}); /*支持设置passive的,将被动模式显式设置为false*/layer.addEventListener('touchstart', this.onTouchStart, false);// 否则,去除默认的被动模式

学到的知识点

 

event.stopImmediatePropagation

我们都知道stopPropagation是阻止默认事件,那stopImmediatePropagation跟stopPropagation最大的区别在于它能够阻止当前元素剩下的监听函数的执行。

 

EventTarget.addEventListener(type, listener[, options])的option参数的passive属性(es6第三个参数可以是对象)

说实话,事件监听用到现在,我一直以为options参数只有一个Boolean来判断在捕获/冒泡阶段监听事件。直到今天看到。 passive(默认false)表示listener永远不会调用preventDefault(),如果仍然调用此函数,客户端会抛出一个控制台警告。而且设置此属性可以改善滚屏性能,具体见MDN。

参考文档:

1: https://github.com/ftlabs/fastclick/blob/master/lib/fastclick.js (fastclick源码)

2: https://github.com/VaJoy/fastclick-analysis/blob/master/fastclick.js

3: fastclick解析与ios11.3相关bug原因分析

4: event.stopImmediatePropagation----MDN;

5: EventTarget.addEventListener()-----MDN

6:移动端Click 300ms点击延迟的来龙去脉

         
本文版权归作者和博客园共有,欢迎转载,共同交流学习。

你真的了解FastClick吗?

前段时间在做公司官网手机端菜单部分的时候,遇到一些很诡异的点击问题。比如菜单点击无效/双击才有效、在手指滑动的时候会触发点击事件、以及同样的事件处理在微信跟浏览器会有不一样的表现等等,这些问题我一直试图用一些移动端事件的hack来解决,到最后还是有两个问题没有解决掉。后来意识到可能是引入的插件导致的事件冲突引起,因为一直都在全局引入了fastclick,以及最初偷懒引入的一个菜单功能插件(插件中有引入iScroll)。经过排查最后得出结论是fastclick与插件 冲突所致,只能去除插件重写菜单功能。而这个小插曲也让我有兴趣阅读一下它的源码来深究一下fastclick到底做了什么?

FastClick的使用场景及背景:

  • 移动端的开发经常需要监听用户的双击行为,事件的发生顺序是这样的:touchstart---touchmove---touchend,然后大约过300ms触发click事件,用来判断是否有双击事件。
  • 在混合使用touch与click时,会导致点击穿透!(此处不展开讨论)
  • FastClick的思路就是利用touch来模拟tap(触碰),如果认为是一次有效的tap,则在touchend时立即模拟一个click事件,分发到事件源(相当于主动触发一次click),同时阻止掉浏览器300ms后产生的click。自然也不存在点击穿透的问题。

众所周知的FastClick用法:

 

Javascript原生

if ('addEventListener' in document) {    document.addEventListener('DOMContentLoaded', function() {        FastClick.attach(document.body);    }, false);}

 

jQuery

$(function() {    FastClick.attach(document.body);});

 

类似Common JS的模块系统方式

var attachFastClick = require('fastclick');attachFastClick(document.body);

 

needsclick

对于页面上不需要使用fastclick来立刻触发点击事件的元素在元素标签的class上添加needsclick

 

不需要使用fastclick的情况

  • PC端,FastClick只在移动端监听;

  • Android版Chrome 32+浏览器,如果设置viewport meta的值为width=device-width,这种情况下浏览器会马上出发点击事件,不会延迟300毫秒。

<meta name="viewport" content="width=device-width, initial-scale=1">

 

  • 所有版本的Android Chrome浏览器,如果设置viewport meta的值有user-scalable=no,浏览器也是会立即触发点击事件。
  • IE11+浏览器设置了css的属性touch-action: manipulation,它会在某些标签(a,button等)禁止双击事件,IE10的为-ms-touch-action: manipulation

FastClick的实现原理

Fastclick的源码中除了对旧版本浏览器的polyfill以及特殊版本浏览器的的bug解决,主要绑定了以下原型方法:

/*构造函数*/function FastClick(layer, options)/*判断是否需要浏览器原生的click事件(针对一些特殊元素比如表单)*/FastClick.prototype.needsClick = function(target)/*判断给定元素是否需要通过合成click事件来模拟聚焦*/FastClick.prototype.needsFocus = function(target)/*合成click事件并在指定元素上触发*/FastClick.prototype.sendClick = function(targetElement, event)/* touchstart */FastClick.prototype.onTouchStart = function(event)/* touchmove*/FastClick.prototype.onTouchMove = function(event)/* touchend*/FastClick.prototype.onTouchEnd = function(event)/*判断这次tap是否有效*/FastClick.prototype.onMouse = function(event) /*click handler 捕获阶段监听*/FastClick.prototype.onClick = function(event)/*移出fastlick事件绑定*/FastClick.prototype.destroy = function()/*调用FastClick*/FastClick.attach = function(layer, options) {    return new FastClick(layer, options);};

先用一张图来解构FastClick源码(图片来自其他博客):

 

初始化的时候主要都做了什么呢?

/*不需要处理的元素类型,则直接返回(这些情况上面已经提到)*/if (FastClick.notNeeded(layer)) {       return;}/*Some old versions of Android don't have Function.prototype.bind*//*对安卓老版本不支持bind的polyfill*/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);}/* 如果layer直接在DOM上写了 onclick 方法,那我们需要把它替换为 addEventListener 绑定形式*/if (typeof layer.onclick === 'function') {    oldOnClick = layer.onclick;    layer.addEventListener('click', function(event) {        oldOnClick(event);    }, false);    layer.onclick = null;}

 

在FastClick.prototype.needsClick中有如下一行代码即是对 needsclick的判断:

 /*元素带了名为“needsclick”的class返回true*/ return (/\bneedsclick\b/).test(target.className);

 

在FastClick.prototype.needsFocus 中有如下一行代码即是对 needsfocus 的判断:

 /*带有名为“needsfocus”的class则返回true*/ return (/\bneedsfocus\b/).test(target.className);

 

touchstart事件

FastClick.prototype.onTouchStart = function(event) {    var targetElement, touch, selection;    /*多指触控手势则忽略*/    if (event.targetTouches.length > 1) {        return true;    }    /*某些旧浏览器,如果target是一个文本节点,得返回其DOM节点*/    targetElement = this.getTargetElementFromEventTarget(event.target);    touch = event.targetTouches[0];    if (deviceIsIOS) {        /*若用户已经选中了一些内容(比如选中了一段文本打算复制),则忽略*/        selection = window.getSelection();        if (selection.rangeCount && !selection.isCollapsed) {            return true;        }        if (!deviceIsIOS4) {             /*             怪异特性处理——若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 里标记的事件时间戳      his.tapDelay 为常量 200 (ms)      此举用来避免 phantom 的双击(200ms内快速点了两次)触发 click      反正200ms内的第二次点击会禁止触发点击的默认事件    */  if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {        event.preventDefault();    }    return true;};

 

touchmove事件

FastClick.prototype.onTouchMove = function(event) {    if (!this.trackingClick) {        return true;    }    /* If the touch has moved, cancel the click tracking*/    if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {        this.trackingClick = false;        this.targetElement = null;    }    return true;};

 

touchend事件

FastClick.prototype.onTouchEnd = function(event) {        var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;        if (!this.trackingClick) {            return true;        }        /*避免双击(200ms内快速点了两次)触发 click*/        if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {            /*该属性会在 onMouse 事件中被判断,为true则彻底禁用事件和冒泡*/            this.cancelNextClick = true;             return true;        }        /*识别是否为长按事件,如果是(大于700ms)则忽略*/        if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {            return true;        }        /*得重置为false,避免input事件被意外取消*/        this.cancelNextClick = false;        /*标记touchend时间,方便下一次的touchstart做双击校验*/        this.lastClickTime = event.timeStamp;        trackingClickStart = this.trackingClickStart;        /*重置 this.trackingClick 和 this.trackingClickStart*/        this.trackingClick = false;        this.trackingClickStart = 0;        /* iOS 6.0-7.*版本下有个问题 —— 如果layer处于transition或scroll过程,event所提供的target是不正确的*/        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事件触发流程)                 另外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);            /*立即触发其click事件*/            this.sendClick(targetElement, event);            /*      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;    };

 

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在IOS11.3以上版本的bug及解决方案

 

1:文本框在内容区点击不会立即定位到相应位置,而是在文本末尾。而长摁超过100ms则无此问题。

首先交互肯定是在focus的时候发生,让我们看下FastClick里的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();        }

在IOS下是通过targetElement.setSelectionRange来定位位置,至于在iOS11.3下为什么会出现这个bug,仍未知,解决的方法简单暴力,直接改写此方法:

FastClick.prototype.focus = function(targetElement) {    targetElement.focus();};

 

2:ios11.3支持了Web API:允许对事件支持被动模式,减少滚动屏幕的性能损耗和奔溃,并且针对document的touch事件监听添加被动模式的配置,因此document将不再调用preventDefault方法。这些改动会引起fastclick的另一个bug,当静置app或锁屏几秒后页面将无法响应任何点击操作。

解决方法:

layer.addEventListener('touchstart', this.onTouchStart, {passive:false}); /*支持设置passive的,将被动模式显式设置为false*/layer.addEventListener('touchstart', this.onTouchStart, false);// 否则,去除默认的被动模式

学到的知识点

 

event.stopImmediatePropagation

我们都知道stopPropagation是阻止默认事件,那stopImmediatePropagation跟stopPropagation最大的区别在于它能够阻止当前元素剩下的监听函数的执行。

 

EventTarget.addEventListener(type, listener[, options])的option参数的passive属性(es6第三个参数可以是对象)

说实话,事件监听用到现在,我一直以为options参数只有一个Boolean来判断在捕获/冒泡阶段监听事件。直到今天看到。 passive(默认false)表示listener永远不会调用preventDefault(),如果仍然调用此函数,客户端会抛出一个控制台警告。而且设置此属性可以改善滚屏性能,具体见MDN。

参考文档:

1: https://github.com/ftlabs/fastclick/blob/master/lib/fastclick.js (fastclick源码)

2: https://github.com/VaJoy/fastclick-analysis/blob/master/fastclick.js

3: fastclick解析与ios11.3相关bug原因分析

4: event.stopImmediatePropagation----MDN;

5: EventTarget.addEventListener()-----MDN

6:移动端Click 300ms点击延迟的来龙去脉

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!