FastClick 源碼解讀

其實一直就想花些時間讀一讀那些優秀的開源庫,今天終於下了決定打算死磕下本身,2016年每月讀2-3個優秀的開源庫,把源碼精彩的地方和本身心得分享給你們。javascript

目錄

(一)背景
(二)源碼解析
(三)Zepto 點擊穿透與 FastClick
(四)新技能 Get
(五)參考文獻html

(一)背景

作前端的必定都知道,原生click事件在移動瀏覽器上會有300毫秒的延遲,會讓用戶以爲卡頓,這300毫秒究竟是怎麼來的呢,估計就不太知道了,再繼續深究這300毫秒的來源是什麼,應該會更少人知道吧。國外有一篇頗有名的文章說的很詳細,有興趣能夠看一下:what-exactly-is.....-the-300ms-click-delay。簡短來講:是移動瀏覽器都支持雙擊縮放或雙擊滾動的操做,因爲當用戶第一次點擊屏幕後,瀏覽器不能馬上判斷用戶確實要打開這個連接,仍是想要進行雙擊的操做,所以幾乎如今全部瀏覽器都效仿Safari當年的約定,在點擊事件上加了300毫秒的延遲。前端

在這個web頁面橫行的年代,每一個點擊事件都有300毫秒的延遲是不容許的。再後來出來了不少的解決辦法,好比Zepto的tap事件(會引起擊穿的bug,後面會着重說),fastclick.js等均可以解決,可是多多少少會有些負做用,綜合起來我最喜歡fastclick的解決方案,今天就來讀一讀它的源碼吧~java

(二)解析

1. 引入 FastClick 到本身的環境

829行:如今通常插件都會用這種方式兼容AMD、commonJs風格、原生Js的方式。還有CMD等這裏沒有兼容,這裏能夠根據本身項目需求稍做修改。android

//優先兼容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入口方法attachgit

//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。github

  1. 全部pc瀏覽器web

  2. 瀏覽器不支持ontouchstartchrome

  3. 安卓中chrome(all versions)meta中有user-scalable="no"屬性segmentfault

  4. 安卓中chrome 32+ meta中有width=device-width 屬性

  5. BlackBerry 10.3+

  6. Firefox 27+

  7. 有-ms-touch-action: manipulation屬性的IE10

  8. 有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毫秒延遲的問題。可是出現了各類擊穿現象

  1. 同頁面tap點擊彈出彈層,彈層中也有一個button,正好重疊的時候,會出現擊穿

  2. 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事件與點擊穿透問題

  1. zepto中的 tap 經過兼聽綁定在 document 上的 touch 事件來完成 tap 事件的模擬的,是經過事件冒泡實現的。在點擊完成時(touchstart / touchend)的 tap 事件須要冒泡到 document 上纔會觸發。而在冒泡到 document 以前,手指接觸和離開屏幕(touchstart / touchend)是會觸發 click 事件的。

  2. 由於 click 事件有延遲(大概是300ms,爲了實現safari的雙擊事件的設計),因此在執行完 tap 事件以後,彈出層立馬就隱藏了,此時 click 事件還在延遲的 300ms 之中。當 300ms 到來的時候,click 到的實際上是隱藏元素下方的元素。

  3. 若是正下方的元素有綁定 click 事件,此時便會觸發,若是沒有綁定 click 事件的話就當沒發生。若是正下方的是 input 輸入框(或是 select / radio / checkbox),點擊默認 focus 而彈出輸入鍵盤,也就出現了上面的「點透」現象。

因此到這裏,我的仍是建議直接使用fastclick.js庫來解決移動端瀏覽器300毫秒的問題,不建議本身寫,坑仍是挺多的,這個庫壓縮後仍是挺小的,能夠用各類方式引用,來替代Zepto中的touch.js插件是個不錯的辦法。

(四)新技能 Get

經過讀這個庫,發現了不少知識上的盲區或者理解的並非很透徹的點,再深化一下~

  1. stopImmediatePropagation 和 stopPropagation 的區別 參考文章

    1. 他們均可以阻止事件冒泡到父元素

    2. stopImmediatePropagation多作了一件事:阻止綁定在該元素上其餘事件運行

(五)參考文獻

  1. 300毫秒的起源:what-exactly-is.....-the-300ms-click-delay

  2. stopImmediatePropagation 和 stopPropagation 的區別:http://segmentfault.com/q/1010000000120125

  3. 也來講說touch事件與點擊穿透問題:http://segmentfault.com/a/1190000003848737

相關文章
相關標籤/搜索