【讀fastclick源碼有感】完全解決tap「點透」,提高移動端點擊響應速度

申明!!!最後發現判斷有誤,各位讀讀就好,正在研究中.....尼瑪水太深了javascript

前言

近期使用tap事件爲老夫帶來了這樣那樣的問題,其中一個問題是解決了點透還須要將原來一個個click變爲tap,這樣的話咱們就拋棄了ie用戶
固然能夠作兼容,可是沒人想動老代碼的,因而今天拿出了fastclick這個東西,html

這是最近第四次發文說tap的點透事件,咱們一直對解決「點透」的蒙版耿耿於懷,因而今天老大提出了一個庫fastclick,最後證實解決了咱們的問題java

並且click沒必要替換爲tap了,因而咱們老大就語重心長的對我說了一句,大家就誤我吧,我郵件都發出去了......node

因而我下午就在看fastclick這個庫,看看是否是能解決咱們的問題,因而咱們開始吧android

讀fastclick源碼

尼瑪使用太簡單了,直接一句:ios

FastClick.attach(document.body);

因而全部的click響應速度直接提高,剛剛的!什麼input獲取焦點的問題也解決了!!!尼瑪若是真的能夠的話,原來改頁面的同事確定會啃了我瀏覽器

一步步來,咱們跟進去,入口就是attach方法:app

1 FastClick.attach = function(layer) { 2  'use strict'; 3  return new FastClick(layer); 4 };

這個兄弟不過實例化了下代碼,因此咱們還要看咱們的構造函數:dom

 function FastClick(layer) {
 'use strict';
 var oldOnClick, self = this;
 this.trackingClick = false;
 this.trackingClickStart = 0;
 this.targetElement = null;
 this.touchStartX = 0;
 this.touchStartY = 0;
 this.lastTouchIdentifier = 0;
 this.touchBoundary = 10;
 this.layer = layer;
 if (!layer || !layer.nodeType) {
  throw new TypeError('Layer must be a document node');
 }
 this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };
 this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };
 this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };
 this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self, arguments); };
 this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };
 this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };
 if (FastClick.notNeeded(layer)) {
  return;
 }
 if (this.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);
 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') {
    adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
     if (!event.propagationStopped) {
      callback(event);
     }
    }), capture);
   } else {
    adv.call(layer, type, callback, capture);
   }
  };
 }
 if (typeof layer.onclick === 'function') {
  oldOnClick = layer.onclick;
  layer.addEventListener('click', function(event) {
   oldOnClick(event);
  }, false);
  layer.onclick = null;
 }
}

複製代碼

看看這段代碼,上面不少屬性幹了什麼事情我也不知道......因而忽略了ide

1 if (!layer || !layer.nodeType) { 
2  throw new TypeError('Layer must be a document node'); 
3 }

其中這裏要注意,咱們必須傳入一個節點給構造函數,不然會出問題

而後這個傢伙將一些基本的鼠標事件註冊在本身的屬性方法上了,具體是幹神馬的咱們後面再說

在後麪點有個notNeeded方法:


FastClick.notNeeded = function(layer) {
 'use strict';
 var metaViewport;
 if (typeof window.ontouchstart === 'undefined') {
  return true;
 }
 if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) {
  if (FastClick.prototype.deviceIsAndroid) {
   metaViewport = document.querySelector('meta[name=viewport]');
   if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) {
    return true;
   }
  } else {
   return true;
  }
 }
 if (layer.style.msTouchAction === 'none') {
  return true;
 }
 return false;
};

複製代碼

這個方法用於判斷是否須要用到fastclick,註釋的意思不太明白,咱們看看代碼吧

首先一句:

1 if (typeof window.ontouchstart === 'undefined') { 
2   return true; 
3 }

若是不支持touchstart事件的話,返回true
PS:如今的只管感覺就是fastclick應該也是以touch事件模擬的,可是其沒有點透問題

後面還判斷了android的一些問題,我這裏就不關注了,意思應該就是支持touch才能支持吧,因而回到主幹代碼

主幹代碼中,咱們看到,若是瀏覽器不支持touch事件或者其它問題就直接跳出了

而後裏面有個deviceIsAndroid的屬性,咱們跟去看看(其實不用看也知道是判斷是不是android設備)

FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;

綁定事件

好了,這傢伙開始綁定註冊事件了,至此還未看出異樣

複製代碼

if (this.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

完了多了一個屬性:

阻止當前事件的冒泡行爲而且阻止當前事件所在元素上的全部相同類型事件的事件處理函數的繼續執行.

若是某個元素有多個相同類型事件的事件監聽函數,則當該類型的事件觸發時,多個事件監聽函數將按照順序依次執行.若是某個監聽函數執行了 event.stopImmediatePropagation()方法,則除了該事件的冒泡行爲被阻止以外(event.stopPropagation方法的做用),該元素綁定的其他相同類型事件的監聽函數的執行也將被阻止.

複製代碼

 <html>
    <head>
        <style>
            p { height: 30px; width: 150px; background-color: #ccf; }
            div {height: 30px; width: 150px; background-color: #cfc; }
        </style>
    </head>
    <body>
        <div>
            <p>paragraph</p>
        </div>
        <script>
            document.querySelector("p").addEventListener("click", function(event)
            {
                alert("我是p元素上被綁定的第一個監聽函數");
            }, false);
            document.querySelector("p").addEventListener("click", function(event)
            {
                alert("我是p元素上被綁定的第二個監聽函數");
                event.stopImmediatePropagation();
                //執行stopImmediatePropagation方法,阻止click事件冒泡,而且阻止p元素上綁定的其餘click事件的事件監聽函數的執行.
            }, false);
            document.querySelector("p").addEventListener("click", function(event)
            {
                alert("我是p元素上被綁定的第三個監聽函數");
                //該監聽函數排在上個函數後面,該函數不會被執行.
            }, false);
            document.querySelector("div").addEventListener("click", function(event)
            {
                alert("我是div元素,我是p元素的上層元素");
                //p元素的click事件沒有向上冒泡,該函數不會被執行.
            }, false);
        </script>
    </body>
</html>

複製代碼複製代碼

 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') {
   adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
    if (!event.propagationStopped) {
     callback(event);
    }
   }), capture);
  } else {
   adv.call(layer, type, callback, capture);
  }
 };
}

複製代碼

而後這傢伙從新定義了下注冊與註銷事件的方法,

咱們先看註冊事件,其中用到了Node的addEventListener,這個Node是個什麼呢?

由此觀之,Node是一個系統屬性,表明咱們的節點吧,因此這裏重寫了註銷的事件

這裏,咱們發現,其實他只對click進行了特殊處理

1 adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { 
2  if (!event.propagationStopped) { 
3   callback(event); 
4  } 
5 }), capture);

其中有個hijacked劫持是幹神馬的就暫時不知道了,估計是在中間是否改寫的意思吧
而後這裏重寫寫了下,hijacked估計是一個方法,就是爲了阻止在一個dom上註冊屢次事件屢次執行的狀況而存在的吧

註銷和註冊差很少咱們就無論了,到此咱們其實重寫了咱們傳入dom的註冊註銷事件了,好像很厲害的樣子,意思之後這個dom調用click事件用的是咱們的,固然這只是我暫時的判斷,具體還要往下讀,並且我以爲如今的判斷不靠譜,因而咱們繼續吧

咱們註銷事件時候能夠用addEventListener 或者 dom.onclick=function(){},因此這裏有了下面的代碼:

複製代碼

1 if (typeof layer.onclick === 'function') { 
2  oldOnClick = layer.onclick; 
3  layer.addEventListener('click', function(event) { 
4   oldOnClick(event); 
5  }, false); 
6  layer.onclick = null; 
7 }

複製代碼

此處,他的主幹流程竟然就完了,意思是他全部的邏輯就在這裏了,不論入口仍是出口應該就是事件註冊了,因而咱們寫個代碼來看看

測試入口

複製代碼

 1 <input type="button" id="addEvent" value="addevent">  
 2 <input type="button" id="addEvent1" value="addevent1">  
 3   
 4 $('#addEvent').click(function () {  
 5     var dom = $('#addEvent1')[0]  
 6     dom.addEventListener('click', function () {  
 7         alert('')  
 8         var s = '';  
 9     }) 
 10 });

複製代碼

咱們來這個斷點看看咱們點擊後幹了什麼,咱們如今點擊按鈕1會爲按鈕2註冊事件:

可是很遺憾,咱們在電腦上不能測試,因此增長了咱們讀代碼的困難,在手機上測試後,發現按鈕2響應很快,可是這裏有點看不出問題
最後alert了一個!Event.prototype.stopImmediatePropagation發現手機和電腦都是false,因此咱們上面搞的東西暫時無用

複製代碼

 1 FastClick.prototype.onClick = function (event) {  
 2     'use strict';  
 3     var permitted;  
 4     alert('終於尼瑪進來了');  
 5     if (this.trackingClick) {  
 6         this.targetElement = null;  
 7         this.trackingClick = false;  
 8         return true;  
 9     } 
 10     if (event.target.type === 'submit' && event.detail === 0) { 
 11         return true; 
 12     } 
 13     permitted = this.onMouse(event); 
 14     if (!permitted) { 
 15         this.targetElement = null; 
 16     } 
 17     return permitted; 
 18 };

複製代碼

而後咱們終於進來了,如今咱們須要知道什麼是trackingClick 了

1 /** 
2 * Whether a click is currently being tracked. 
3 * @type boolean 
4 */ 
5 this.trackingClick = false;

咱們最初這個屬性是false,可是到這裏就設置爲true了,就直接退出了,說明綁定事件終止,算了這個咱們暫時不關注,咱們乾點其它的,
由於,我以爲重點仍是應該在touch事件上

PS:到這裏,咱們發現這個庫應該不僅是將click加快,而是全部的響應都加快了

我在各個事件部分log出來東西,發現有click的地方都只執行了touchstart與touchend,因而至此,我以爲個人觀點成立
他使用touch事件模擬量click,因而咱們就只跟進這一塊就好:

複製代碼

FastClick.prototype.onTouchStart = function (event) {
    'use strict';
    var targetElement, touch, selection;
    log('touchstart');
    if (event.targetTouches.length > 1) {
        return true;
    }
    targetElement = this.getTargetElementFromEventTarget(event.target);
    touch = event.targetTouches[0];
    if (this.deviceIsIOS) {
        selection = window.getSelection();
        if (selection.rangeCount && !selection.isCollapsed) {
            return true;
        }
        if (!this.deviceIsIOS4) {
            if (touch.identifier === this.lastTouchIdentifier) {
                event.preventDefault();
                return false;
            }
            this.lastTouchIdentifier = touch.identifier;
            this.updateScrollParent(targetElement);
        }
    }
    this.trackingClick = true;
    this.trackingClickStart = event.timeStamp;
    this.targetElement = targetElement;
    this.touchStartX = touch.pageX;
    this.touchStartY = touch.pageY;
    if ((event.timeStamp - this.lastClickTime) < 200) {
        event.preventDefault();
    }
    return true;
};

複製代碼

其中用到了一個方法:

複製代碼

1 FastClick.prototype.getTargetElementFromEventTarget = function (eventTarget) { 
2     'use strict'; 
3     if (eventTarget.nodeType === Node.TEXT_NODE) { 
4         return eventTarget.parentNode; 
5     } 
6     return eventTarget; 7 };

複製代碼

他是獲取咱們當前touchstart的元素

而後將鼠標的信息記錄了下來,他記錄鼠標信息主要在後面touchend時候根據x、y判斷是否爲click
是ios狀況下還搞了一些事情,我這裏跳過去了
而後這裏記錄了一些事情就跳出去了,沒有特別的事情,如今咱們進入咱們的出口touchend

複製代碼

FastClick.prototype.onTouchEnd = function (event) {
    'use strict';
    var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
    log('touchend');
    if (!this.trackingClick) {
        return true;
    }
    if ((event.timeStamp - this.lastClickTime) < 200) {
        this.cancelNextClick = true;
        return true;
    }
    this.lastClickTime = event.timeStamp;
    trackingClickStart = this.trackingClickStart;
    this.trackingClick = false;
    this.trackingClickStart = 0;
    if (this.deviceIsIOSWithBadTarget) {
        touch = event.changedTouches[0];
        targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
        targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
    }
    targetTagName = targetElement.tagName.toLowerCase();
    if (targetTagName === 'label') {
        forElement = this.findControl(targetElement);
        if (forElement) {
            this.focus(targetElement);
            if (this.deviceIsAndroid) {
                return false;
            }
            targetElement = forElement;
        }
    } else if (this.needsFocus(targetElement)) {
        if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) {
            this.targetElement = null;
            return false;
        }
        this.focus(targetElement);
        if (!this.deviceIsIOS4 || targetTagName !== 'select') {
            this.targetElement = null;
            event.preventDefault();
        }
        return false;
    }
    if (this.deviceIsIOS && !this.deviceIsIOS4) {
        scrollParent = targetElement.fastClickScrollParent;
        if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
            return true;
        }
    }
    if (!this.needsClick(targetElement)) {
        event.preventDefault();
        this.sendClick(targetElement, event);
    }
    return false;
};

複製代碼

這個傢伙洋洋灑灑幹了許多事情

這裏糾正一個錯誤,他onclick那些東西如今也執行了......多是我屏幕有變化(滑動)致使

1 if ((event.timeStamp - this.lastClickTime) < 200) { 
2  this.cancelNextClick = true; 
3  return true; 
4 }

這個代碼很關鍵,咱們首次點擊會執行下面的邏輯,若是連續點擊就直接完蛋,下面的邏輯丫的不執行了......
這個不執行了,那麼這個勞什子又幹了什麼事情呢?
事實上下面就沒邏輯了,意思是若是確實點擊過快,兩次點擊只會執行一次,這個閥值爲200ms,這個暫時看來是沒有問題的

好了,咱們繼續往下走,因而我意識到又到了一個關鍵點
由於咱們用tap事件不能使input得到焦點,可是fastclick卻能得到焦點,這裏也許是一個關鍵,咱們來看看幾個與獲取焦點有關的函數

複製代碼

 1 FastClick.prototype.focus = function (targetElement) {  
 2     'use strict';  
 3     var length;  
 4     if (this.deviceIsIOS && targetElement.setSelectionRange) {  
 5         length = targetElement.value.length;  
 6         targetElement.setSelectionRange(length, length);  
 7     } else {  
 8         targetElement.focus();  
 9     } 
 10 };

複製代碼

setSelectionRange是咱們的關鍵,也許他是這樣獲取焦點的......具體我還要下來測試,留待下次處理吧
而後下面若是時間間隔過長,代碼就不認爲操做的是同一dom結構了
最後迎來了本次的關鍵:sendClick,不管是touchend仍是onMouse都會匯聚到這裏

複製代碼

 1 FastClick.prototype.sendClick = function (targetElement, event) {  
 2     'use strict';  
 3     var clickEvent, touch;  
 4     // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)  
 5     if (document.activeElement && document.activeElement !== targetElement) {  
 6         document.activeElement.blur();  
 7     }  
 8     touch = event.changedTouches[0];  
 9     // Synthesise a click event, with an extra attribute so it can be tracked 
 10     clickEvent = document.createEvent('MouseEvents'); 
 11     clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); 
 12     clickEvent.forwardedTouchEvent = true; 
 13     targetElement.dispatchEvent(clickEvent); 
 14 };

複製代碼

他建立了一個鼠標事件,而後dispatchEvent事件(這個與fireEvent相似)

複製代碼

//document上綁定自定義事件ondataavailable
document.addEventListener('ondataavailable', function (event) {
alert(event.eventType);
}, false);
var obj = document.getElementById("obj");
//obj元素上綁定click事件
obj.addEventListener('click', function (event) {
alert(event.eventType);
}, false);
//調用document對象的 createEvent 方法獲得一個event的對象實例。
var event = document.createEvent('HTMLEvents');
// initEvent接受3個參數:
// 事件類型,是否冒泡,是否阻止瀏覽器的默認行爲
event.initEvent("ondataavailable", true, true);
event.eventType = 'message';
//觸發document上綁定的自定義事件ondataavailable
document.dispatchEvent(event);
var event1 = document.createEvent('HTMLEvents');
event1.initEvent("click", true, true);
event1.eventType = 'message';
//觸發obj元素上綁定click事件
document.getElementById("test").onclick = function () {
obj.dispatchEvent(event1);
};

複製代碼

至此,咱們就知道了,咱們爲dom先綁定了鼠標事件,而後touchend時候觸發了,而至於爲何自己註冊的click未觸發就要回到上面代碼了

解決「點透」(成果)

有了這個思路,咱們來試試咱們抽象出來的代碼:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <style>
        #list { display: block; position: absolute; top: 100px; left: 10px; width: 200px; height: 100px; }
        div { display: block; border: 1px solid black; height: 300px; width: 100%; }
        #input { width: 80px; height: 200px; display: block; }
    </style>
</head>
<body>
    <div id="list" style="background: gray;">
    </div>
    <div id="wrapper">
        <div id="d">
            <input type="text" id="input" />
        </div>
    </div>
    <script type="text/javascript">
        var el = null;
        function getEvent(el, e, type) {
            e = e.changedTouches[0];
            var event = document.createEvent('MouseEvents');
            event.initMouseEvent(type, true, true, window, 1, e.screenX, e.screenY, e.clientX, e.clientY, false, false, false, false, 0, null);
            event.forwardedTouchEvent = true;
            return event;
        }
        list.addEventListener('touchstart', function (e) {
            var firstTouch = e.touches[0]
            el = firstTouch.target;
            t1 = e.timeStamp;
        })
        list.addEventListener('touchend', function (e) {
            e.preventDefault();
            var event = getEvent(el, e, 'click');
            el.dispatchEvent(event);
        })
        var list = document.getElementById('list');
        list.addEventListener('click', function (e) {
            list.style.display = 'none';
            setTimeout(function () {
                list.style.display = '';
            }, 1000);
        })
    </script>
</body>
</html>

這樣的話,便不會點透了,這是由於zepto touch事件所有綁定值document,因此 e.preventDefault();無用
結果咱們這裏是直接在dom上,e.preventDefault();
便起了做用不會觸發瀏覽器默認事件,因此也不存在點透問題了,至此點透事件告一段落......

幫助理解的圖

代碼在公司寫的,回家後不知道圖上哪裏了,各位將就看吧

爲何zepto會點透/fastclick如何解決點透

我最開始就給老大說zepto處理tap事件不夠好,搞了不少事情出來

由於他事件是綁定到document上,先touchstart而後touchend,根據touchstart的event參數判斷該dom是否註冊了tap事件,有就觸發

因而問題來了,zepto的touchend這裏有個event參數,咱們event.preventDefault(),這裏原本都是最上層了,這就代碼壓根沒什麼用

可是fastclick處理辦法不可謂不巧妙,這個庫直接在touchend的時候就觸發了dom上的click事件而替換了原本的觸發時間

意思是原來要350-400ms執行的代碼忽然就移到了50-100ms,而後這裏雖然使用了touch事件可是touch事件是綁定到了具體dom而不是document上

因此e.preventDefault是有效的,咱們能夠阻止冒泡,也能夠阻止瀏覽器默認事件,這個纔是fastclick的精華部分,不可謂不高啊!!!

整個fastclick代碼讀來醍醐灌頂,今天收穫很大,在此記錄

後記

上面的說法有點問題,這修正一下:

首先,咱們回到原來的zepto方案,看看他有什麼問題:

  1.  由於js標準本不支持tap事件,因此zepto taptouchstarttouchend模擬而出

  2.  zepto在初始化時便給document綁定touch事件,在咱們點擊時根據event參數得到當前元素,並會保存點下和離開時候的鼠標位置

  3.  根據當前元素鼠標移動範圍判斷是否爲類點擊事件,若是是便觸發已經註冊好的tap事件

 

而後fastclick處理比較與zepto基本一致,可是又有所不一樣

  1.  fastclick是將事件綁定到你傳的元素(通常是document.body

② 在touchstarttouchend後(會手動獲取當前點擊el),若是是類click事件便手動觸發了dom元素的click事件

因此click事件在touchend便被觸發,整個響應速度就起來了,觸發實際與zepto tap同樣

 

好了,爲何基本相同的代碼,zepto會點透而fastclick不會呢?

緣由是zepto的代碼裏面有個settimeout,而就算在這個代碼裏面執行e.preventDefault()也不會有用

這就是根本區別,由於settimeout會將優先級較低

有了按期器,當代碼執行到setTimeout的時候, 就會把這個代碼放到JS的引擎的最後面 

而咱們代碼會立刻檢測到e.preventDefault,一旦加入settimeoute.preventDefault便不會生效,這是zepto點透的根本緣由

結語

雖然,此次走了不少彎路,可是最後終於解決了問題

相關文章
相關標籤/搜索