最近产品妹子提出了一个体验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; } }
内容版权声明:除非注明,否则皆为本站原创文章。