移動端觸摸、點擊事件優化(fastclick源碼學習)

移動端觸摸、點擊事件優化(fastclick源碼學習)

最近在作一些微信移動端的頁面,在此記錄關於移動端觸摸和點擊事件的學習優化過程,主要內容圍繞fastclick展開。
fastclick githubhtml

問題起源

移動端瀏覽器通常在用戶點擊屏幕以後會延遲大約300ms才觸發click event
—— GOOGLE

手機打開此連接查看延遲demo
(如今許多瀏覽器已經不存在延遲問題了,詳見fastclick github,但筆者的手機瀏覽器仍是出現了三百毫秒延遲的問題)
截圖以下
圖片描述node

爲何會300ms延遲呢,主要是有一個雙擊縮放功能,瀏覽器須要判斷用戶點擊是否爲雙擊縮放。這個問題不解決,
一、用戶體驗就會不好,很不流暢,尤爲是在密集操做場景下,好比計算器,不解決300ms延遲問題,感受反應很慢;
二、點擊穿透問題git

事件觸發順序

在瞭解fastclick的思路以前,咱們先看一下事件觸發順序是怎樣的github

  • touchstart
  • touchmove
  • touchend
  • mouseover :當指針設備移動到存在監聽器的元素或其子元素的時候,mouseover事件就會被觸發。
  • mouseenter:當指針設備( 一般指鼠標 )在元素上移動時, mousemove 事件被觸發。
  • mousedown
  • click

移動端click有300ms延遲問題,touch可沒有哦。chrome

fastclick思路

fastclick的思路就是利用touch來模擬tap(觸碰),若是認爲是一次有效的tap,則在touchend時當即模擬一個click事件,分發到事件源(至關於主動觸發一次click),同時阻止掉瀏覽器300ms後產生的click。瀏覽器

源碼學習

先看使用示例,很簡單,咱們的思路就一直跟着attach走。微信

if ('addEventListener' in document) {
    document.addEventListener('DOMContentLoaded', function() {
        FastClick.attach(document.body);
    }, false);
}

直接給body綁定fastlick就好了- -。
看源代碼結構(注:如下全部代碼均去掉了一些不影響理解思路的部分,大部分思路寫在註釋中)app

//構造函數
    function FastClick(layer, options)
//判斷是否須要瀏覽器原生的click事件(針對一些特殊元素好比表單)
    FastClick.prototype.needsClick = function(target)
//發送模擬的click event
    FastClick.prototype.sendClick = function(targetElement, event)
// touchstart eventhandler
    FastClick.prototype.onTouchStart = function(event)
// touchmove eventhandler
    FastClick.prototype.onTouchMove = function(event)
// touchend eventhandler
    FastClick.prototype.onTouchEnd = function(event)
// 判斷此次tap是否有效
    FastClick.prototype.onMouse = function(event) 
//click handler 捕獲階段監聽
    FastClick.prototype.onClick = function(event)
//銷燬fastlick,移除事件綁定
    FastClick.prototype.destroy = function()
//綁定接口
    FastClick.attach = function(layer, options) {
        return new FastClick(layer, options);
    };

attach實際就執行了構造函數進行初始化,接下來咱們來看構造函數發生了什麼less

function FastClick(layer,options){
        //一些屬性初始化
        //安卓一些老版本瀏覽器不支持bind, poly fill
        function bind (method, context) {
          return function () {
            return method.apply(context, arguments);
          };
        }
        var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 
        'onTouchEnd', 'onTouchCancel'];
        var context = this;
        //將全部handler的this綁定到fastclick實例
        for (var i = 0, l = methods.length; i < l; i++) {
            context[methods[i]] = bind(context[methods[i]], context);
        }
        //爲當前fast click對象綁定的layer(咱們的示例中時document.body)加監聽
        layer.addEventListener('click', this.onClick, true);//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);
    }

構造函數主要是初始化一些屬性,polyfill,和添加監聽,
下面開始看一下重頭戲,touchstart,touchend是如何判斷tap是否有效、如何模擬click事件、如何阻止300ms後的click
touchstartide

FastClick.prototype.onTouchStart = function (event) {
    var targetElement, touch, selection;

    // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
    // 若是多觸點多是在縮放,不對targetElement初始化,在此提早終止避免誤模擬產生click
    if (event.targetTouches.length > 1) {
      return true;
    }

    //獲取發生事件源元素(目標階段的元素)
    targetElement = this.getTargetElementFromEventTarget(event.target);
    touch = event.targetTouches[0];
    
    this.trackingClick = true;//標記開始跟蹤click
    this.trackingClickStart = event.timeStamp;//開始跟蹤時間
    this.targetElement = targetElement;//事件源元素

    //觸摸座標,接下來判斷是否越界用到
    this.touchStartX = touch.pageX;
    this.touchStartY = touch.pageY;

    // Prevent phantom clicks on fast double-tap (issue #36)
    if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
      event.preventDefault();//阻止以後的click
    }

    return true;
  };

touchstart主要是初始化跟蹤的tap相關的一些屬性,用於以後的判斷‘
接下來touchmove

FastClick.prototype.onTouchMove = function (event) {
    if (!this.trackingClick) {
      return true;
    }

    // If the touch has moved, cancel the click tracking 移動到了其餘元素
    if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {//移動越界了,取消本次click模擬處理,走原生流程
      this.trackingClick = false;
      this.targetElement = null;
    }

    return true;
  };

touchmove比較簡單,主要是兼容滑動tap(swiper)等等,滑動越界則不模擬click
下面是touchend

FastClick.prototype.onTouchEnd = function (event) {
    var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;

    if (!this.trackingClick) {
      return true;
    }

    // Prevent phantom clicks on fast double-tap (issue #36)
    //阻止快速雙擊
    if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
      this.cancelNextClick = true;
      return true;
    }
    //超時就不算click了,走原生流程,不阻止click
    if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
      return true;
    }

    this.lastClickTime = event.timeStamp;

    this.trackingClick = false;
    this.trackingClickStart = 0;



    // Prevent the actual click from going though - unless the target node is marked as requiring
    // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
    if (!this.needsClick(targetElement)) {
      event.preventDefault();//阻止以後的click
      this.sendClick(targetElement, event);//發送模擬click
    }

    return false;
  };
 //發送模擬的click event
  FastClick.prototype.sendClick = function (targetElement, event) {
    var clickEvent, touch;

    // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
    if (document.activeElement && document.activeElement !== targetElement) {
      document.activeElement.blur();
    }

    touch = event.changedTouches[0];

    //模擬click
    // Synthesise a click event, with an extra attribute so it can be 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;
    //向targetElement分發模擬的click
    targetElement.dispatchEvent(clickEvent);
  };

最後,還會在layer的click捕獲階段監聽

//click handler 捕獲階段監聽
  FastClick.prototype.onClick = function (event) {
    var permitted;
    // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
    if (this.trackingClick) {//一、出界會置爲false,2成功模擬了一次完成tap並阻止click也會置爲false,三、避免三方庫影響
      this.targetElement = null;
      this.trackingClick = false;
      return true;
    }

    // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
    if (event.target.type === 'submit' && event.detail === 0) {
      return true;
    }

    permitted = this.onMouse(event);

    // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
    if (!permitted) {
      this.targetElement = null;
    }

    // If clicks are permitted, return true for the action to go through.
    return permitted;
  };

 // 判斷此次鼠標是否有效
  FastClick.prototype.onMouse = function (event) {

    // If a target element was never set (because a touch event was never fired) allow the event
    if (!this.targetElement) {
      return true;
    }

    // 標記fastclick模擬產生的event
    if (event.forwardedTouchEvent) {
      return true;
    }

    // Programmatically generated events targeting a specific element should be permitted
    if (!event.cancelable) {
      return true;
    }

    // Derive and check the target element to see whether the mouse event needs to be permitted;
    // unless explicitly enabled, prevent non-touch click events from triggering actions,
    // to prevent ghost/doubleclicks.
    // 是否須要原生的click
    if (!this.needsClick(this.targetElement) || this.cancelNextClick) {

      // Prevent any user-added listeners declared on FastClick element from being fired.
      if (event.stopImmediatePropagation) {
        event.stopImmediatePropagation();
      } else {

        // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
        event.propagationStopped = true;
      }

      // Cancel the event 阻止事件捕獲和冒泡
      event.stopPropagation();
      event.preventDefault();

      return false;
    }

    // If the mouse event is permitted, return true for the action to go through.
    return true;
  };

這裏主要是判斷此次click是否有效(如無效,則阻止捕獲和冒泡)
至此基本流程已經結束。
其中有1個注意的點,筆者在chrome(Version 64.0.3282.119 (Official Build) (64-bit))已測試
stopPropagation,stopImmediatePropagation不只會阻止冒泡還會阻止捕獲過程哦。

最後

推薦閱讀源碼,源碼中有許多關於focus、不一樣瀏覽器兼容和特殊表單元素的處理fastclick github。
這裏是筆者帶有中文註釋的代碼中文註釋代碼。
若有紕漏,歡迎批評指正。

Reference

MDN
https://juejin.im/entry/55d73...

相關文章
相關標籤/搜索