FastClick 源碼解讀
其實一直就想花些時間讀一讀那些優秀的開源庫,今天終于下了決定打算死磕下自己,2016年每個月讀2-3個優秀的開源庫,把源碼精彩的地方和自己心得分享給大家。
目錄
(一)背景
(二)源碼解析
(三)Zepto 點擊穿透與 FastClick
(四)新技能 Get
(五)參考文獻
(一)背景
做前端的一定都知道,原生click事件在移動瀏覽器上會有300毫秒的延遲,會讓用戶覺得卡頓,這300毫秒到底是怎么來的呢,估計就不太知道了,再繼續深究這300毫秒的來源是什么,應該會更少人知道吧。國外有一篇很有名的文章說的很詳細,有興趣可以看一下: what-exactly-is.....-the-300ms-click-delay 。簡短來說:是移動瀏覽器都支持雙擊縮放或雙擊滾動的操作,由于當用戶第一次點擊屏幕后,瀏覽器不能立刻判斷用戶確實要打開這個鏈接,還是想要進行雙擊的操作,因此幾乎現在所有瀏覽器都效仿Safari當年的約定,在點擊事件上加了300毫秒的延遲。
在這個web頁面橫行的年代,每個點擊事件都有300毫秒的延遲是不允許的。再后來出來了很多的解決辦法,比如Zepto的tap事件(會引發擊穿的bug,后面會著重說),fastclick.js等都可以解決,但是多多少少會有些負作用,綜合起來我最喜歡 fastclick 的解決方案,今天就來讀一讀它的源碼吧~
(二)解析
1. 引入 FastClick 到自己的環境
829行 :現在一般插件都會用這種方式兼容AMD、commonJs風格、原生Js的方式。還有CMD等這里沒有兼容,這里可以根據自己項目需求稍作修改。
//優先兼容AMD方式 if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { define(function() { return FastClick; }); } else if (typeof module !== 'undefined' && module.exports) { //兼容commonJs風格 module.exports = FastClick.attach; module.exports.FastClick = FastClick; } else { //最后兼容原生Js window.FastClick = FastClick; }
2. 入口
824行 :FastClick入口方法attach
//layer參數:要監聽的dom對象,一般是document.body //options參數:用來覆蓋自定義參數,個人建議不去覆蓋, //因為里面的參數設定都是FastClick的精華, //比如規定了touchstart和touchend事件之間的200毫秒最小間隔。 FastClick.attach = function(layer, options) { return new FastClick(layer, options); };
3. FastClick 函數
1. 23-103行 :設置默認值
//比如這幾個參數,上面提到不建議自定義覆蓋, //這些參數正是FastClick的精華所在, //大幅度修改數值可能讓整個庫的功效大打折扣。 this.touchBoundary = options.touchBoundary || 10; this.tapDelay = options.tapDelay || 200; this.tapTimeout = options.tapTimeout || 700;
2. 105-107行 :判斷是否需要調用FastClick
官網上 when-it-isnt-needed 說的很清楚,以下情況不需要FastClick。
-
所有pc瀏覽器
-
瀏覽器不支持ontouchstart
-
安卓中chrome(all versions)meta中有user-scalable="no"屬性
-
安卓中chrome 32+ meta中有width=device-width 屬性
-
BlackBerry 10.3+
-
Firefox 27+
-
有-ms-touch-action: manipulation屬性的IE10
-
有touch-action: manipulation屬性的IE11
//所以在不需要FastClick的瀏覽器會直接return掉, //不會執行下面的所有代碼。 if (FastClick.notNeeded(layer)) { return; }
3. 110-132行 :自定義函數綁定在對應默認事件上
layer.addEventListener('touchstart', this.onTouchStart, false); layer.addEventListener('touchmove', this.onTouchMove, false); layer.addEventListener('touchend', this.onTouchEnd, false); layer.addEventListener('touchcancel', this.onTouchCancel, false);
4. 137-159行 :對舊版本android不支持 stopImmediatePropagation 事件的兼容
這里有一個知識點: stopImmediatePropagation和stopPropagation的區別 ,后面總結會詳細說。
5. 164-173行 :兼容直接綁定在dom上的onclick事件
//把<body onclick="fun()"></body> 直接綁定在dom上的onclick事件 //改為綁定在該dom上的形式, //為了之后的模擬點擊事件。 if (typeof layer.onclick === 'function') { oldOnClick = layer.onclick; layer.addEventListener('click', function(event) { oldOnClick(event); }, false); layer.onclick = null; }
4. 兼容 & 判斷
181-219行 :瀏覽器UA判斷
311-319行 :determineEventType 兼容安卓chrome中的select框事件從click改為mousedown
325-355行 :focus 兼容蘋果手機setSelectionRange不能正確獲取焦點的bug
343-367行 :updateScrollParent (待看)
374-382行 :getTargetElementFromEventTarget 兼容獲取點擊元素,iOS 4.1中會獲取文字作為焦點,取它的父元素dom
497-512行 :findControl
//點擊label的時候,找到他對應的元素,并獲取焦點 <label for="input"></label> <input id="input"/>
459-467 :touchHasMoved 手指點擊時移動間距大于10px,返回true
476-488 :onTouchMove 手指點擊時移動間距大于10px,即視為touchmove,不觸發模擬click事件
5. 進階方法
一般情況下用不到,以下方法,特殊需求可能會用到。
227-254行 :needsClick 確定哪些元素需要原生的click事件
263-285行 :needsFocus 確定哪些元素需要原生的focus事件
//如果哪些元素需要使用原生的click或者是focus事件,需要在dom上加上class='needsClick' <a class="needsclick">Ignored by FastClick</a>
712-726行 :destroy 這個方法只在源碼中,如果有需求銷毀事件,重構源碼時可以調用這個方法。
6. 核心方法
391-450 :onTouchStart
FastClick.prototype.onTouchStart = function(event) { //tapDelay默認300毫秒,點擊時間差小于300毫秒,則阻止事件再次觸發,阻止短時間內雙擊的問題 if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { event.preventDefault(); } }
521-610 :onTouchEnd
if (!this.needsClick(targetElement)) { // 如果這不是一個需要使用原生click的元素,則屏蔽原生事件,避免觸發兩次click event.preventDefault(); // 觸發一次模擬的click this.sendClick(targetElement, event); }
294-309 :sendClick(核心方法)
//這個事件會在onTouchEnd中用到,經過一系列的判斷,符合條件,調用這個模擬事件 FastClick.prototype.sendClick = function(targetElement, event) { var clickEvent, touch; //創建一個鼠標事件 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); //觸發這個事件 targetElement.dispatchEvent(clickEvent); };
(三)Zepto 點擊穿透與 FastClick
最近項目中在用Zepto的插件touch.js中tap事件,來解決移動瀏覽器中300毫秒延遲的問題。但是出現了各種擊穿現象
-
同頁面tap點擊彈出彈層,彈層中也有一個button,正好重疊的時候,會出現擊穿
-
tap事件點擊,頁面跳轉,新頁面中同位置也有一個按鈕,會出現擊穿
我們可以看下Zepto對 singleTap 事件的處理。見 源碼 136-143 行 ,可以看出在 touchend 響應 250ms 無操作后,則觸發singleTap。
//trigger single tap after 250ms of inactivity else { touchTimeout = setTimeout(function(){ touchTimeout = null if (touch.el) touch.el.trigger('singleTap') touch = {} }, 250) }
用這篇文章里面的一句話來解釋下Zepto的穿透問題 也來說說touch事件與點擊穿透問題
-
zepto中的 tap 通過兼聽綁定在 document 上的 touch 事件來完成 tap 事件的模擬的,是通過事件冒泡實現的。在點擊完成時(touchstart / touchend)的 tap 事件需要冒泡到 document 上才會觸發。而在冒泡到 document 之前,手指接觸和離開屏幕(touchstart / touchend)是會觸發 click 事件的。
-
因為 click 事件有延遲(大概是300ms,為了實現safari的雙擊事件的設計),所以在執行完 tap 事件之后,彈出層立馬就隱藏了,此時 click 事件還在延遲的 300ms 之中。當 300ms 到來的時候,click 到的其實是隱藏元素下方的元素。
-
如果正下方的元素有綁定 click 事件,此時便會觸發,如果沒有綁定 click 事件的話就當沒發生。如果正下方的是 input 輸入框(或是 select / radio / checkbox),點擊默認 focus 而彈出輸入鍵盤,也就出現了上面的“點透”現象。
所以到這里,個人還是建議直接使用fastclick.js庫來解決移動端瀏覽器300毫秒的問題,不建議自己寫,坑還是挺多的,這個庫壓縮后還是挺小的,可以用各種方式引用,來替代Zepto中的touch.js插件是個不錯的辦法。
(四)新技能 Get
通過讀這個庫,發現了很多知識上的盲區或者理解的并不是很透徹的點,再深化一下~
-
stopImmediatePropagation 和 stopPropagation 的區別參考文章
-
他們都可以阻止事件冒泡到父元素
-
stopImmediatePropagation多做了一件事:阻止綁定在該元素上其他事件運行
-
(五)參考文獻
-
stopImmediatePropagation 和 stopPropagation 的區別: http://segmentfault.com/q/1010000000120125
-
也來說說touch事件與點擊穿透問題: http://segmentfault.com/a/1190000003848737