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