FastClick-源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
;
(function() {
'use strict';
//构造函数
function FastClick(layer, options) {
var oldOnClick;
options = options || {};

//是否开始追踪click事件
this.trackingClick = false;

//存储第一次按下时间戳
this.trackingClickStart = 0;

//目标元素
this.targetElement = null;

//存放坐标值X
this.touchStartX = 0;

//存放坐标值Y
this.touchStartY = 0;

//主要hack iOS4下的一个怪异问题
this.lastTouchIdentifier = 0;

//用于区分是click还是Touchmove,若出点移动超过该值则视为touchmove
this.touchBoundary = options.touchBoundary || 10;

// 绑定了FastClick的元素,一般是是body
this.layer = layer;

//双击最小点击时间差
this.tapDelay = options.tapDelay || 200;

//长按最大时间
this.tapTimeout = options.tapTimeout || 700;

//如果是属于不需要处理的元素类型,则直接返回
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;
}
}

/**
* Windows Phone 8.1 fakes user agent string to look like Android and iPhone.
*
* @type boolean
*/
var deviceIsWindowsPhone = navigator.userAgent.indexOf("Windows Phone") >= 0;

/**
* Android requires exceptions.
*
* @type boolean
*/
var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone;

/**
* iOS requires exceptions.
*
* @type boolean
*/
var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone;

/**
* iOS 4 requires an exception for select elements.
*
* @type boolean
*/
var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);

/**
* iOS 6.0-7.* requires the target element to be manually derived
*
* @type boolean
*/
var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_\d/).test(navigator.userAgent);

/**
* BlackBerry requires exceptions.
*
* @type boolean
*/
var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;

//判断元素是否要保留穿透功能
FastClick.prototype.needsClick = function(target) {
switch(target.nodeName.toLowerCase()) {

// disabled的input
case 'button':
case 'select':
case 'textarea':
if(target.disabled) {
return true;
}

break;
case 'input':

// file组件必须通过原生click事件点击才有效
if((deviceIsIOS && target.type === 'file') || target.disabled) {
return true;
}

break;
case 'label':
case 'iframe':
case 'video':
return true;
}

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

//判断给定元素是否需要通过合成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);
}
};

//合成一个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';
};

//设置元素聚焦事件
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();
}
};

/**
* 检查target是否一个滚动容器里的子元素,如果是则给它加个标记
*/
FastClick.prototype.updateScrollParent = function(targetElement) {
var scrollParent, parentElement;

scrollParent = targetElement.fastClickScrollParent;

// Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
// target element was moved to another parent.
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;
}
};

/**
* 返回目标元素
*/
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {

// 一些较老的浏览器,target 可能会是一个文本节点,得返回其DOM节点
if(eventTarget.nodeType === Node.TEXT_NODE) {
return eventTarget.parentNode;
}

return eventTarget;
};

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.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;
};

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;
};

//找到label标签所映射的组件,方便让用户点label的时候直接激活该组件
FastClick.prototype.findControl = function(labelElement) {

// 有缓存则直接读缓存着的
if(labelElement.control !== undefined) {
return labelElement.control;
}

// 获取指向的组件
if(labelElement.htmlFor) {
return document.getElementById(labelElement.htmlFor);
}

// 没有for属性则激活页面第一个组件(labellable 元素)
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
};

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;
};

FastClick.prototype.onTouchCancel = function() {
this.trackingClick = false;
this.targetElement = null;
};

//用于决定是否允许穿透事件(触发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);
};

//是否没必要使用到 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;
};

FastClick.attach = function(layer, options) {
return new FastClick(layer, options);
};

if(typeof define === 'function' && typeof define.amd === 'object' && define.amd) {

// AMD. Register as an anonymous module.
define(function() {
return FastClick;
});
} else if(typeof module !== 'undefined' && module.exports) {
module.exports = FastClick.attach;
module.exports.FastClick = FastClick;
} else {
window.FastClick = FastClick;
}
}());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Demo</title>
<script src="./fastclick.js"></script>
<style>
div {
width: 200px;
background: red;Y
margin: 0 auto;
height: 200px;
color: wheat;
font-size: 25px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>

<body>
<div id="main">FastClick</div>

<script>
FastClick.attach(document.body);
document.getElementById("main").addEventListener("click", function(event) {
console.log(event.target.innerText)
}, false)
</script>
</body>

</html>