溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

FastClick的示例分析

發(fā)布時(shí)間:2021-07-28 09:48:57 來源:億速云 閱讀:155 作者:小新 欄目:web開發(fā)

小編給大家分享一下FastClick的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

用 iOS 在手Q閱讀書友交流區(qū)發(fā)表書評時(shí),光標(biāo)點(diǎn)擊總是不好定位到正確的位置:

FastClick的示例分析

如上圖,具體表現(xiàn)是較快點(diǎn)擊時(shí),光標(biāo)總會(huì)跳到 textarea 內(nèi)容的尾部。只有當(dāng)點(diǎn)擊停留時(shí)間較久一點(diǎn)(比如超過150ms)才能把光標(biāo)正常定位到正確的位置。

一開始我以為是 iOS 原生的交互問題沒太在意,但后來發(fā)現(xiàn)訪問某些頁面又是沒有這種奇怪體驗(yàn)的。

然后懷疑是否 JS 注冊了某些事件導(dǎo)致的問題,于是試著把業(yè)務(wù)模塊移除了再跑一遍,發(fā)現(xiàn)問題照舊。

于是只好繼續(xù)做排除法,把頁面上的一些庫一點(diǎn)點(diǎn)移掉再運(yùn)行頁面,結(jié)果發(fā)現(xiàn)搗亂的小鬼果然是嫌疑最大的 Fastclick。

然后呢,我試著按API所說,給 textarea 加上一個(gè)名為“needsclick”的類名,希望能繞過 fastclick 的處理直接走原生點(diǎn)擊事件,結(jié)果訝異地發(fā)現(xiàn)屁用沒有。。。

對此感謝后面我們小組的 kindeng 童鞋幫忙研究了下并提供了解決方案,不過我還想進(jìn)一步研究到底是什么原因?qū)е铝诉@個(gè)坑、Fastclick 對我的頁面做了神馬~

所以昨晚花了點(diǎn)時(shí)間一口氣把源碼都蹂躪了一遍。

這會(huì)是一篇很長的文章,但會(huì)是注釋非常詳盡的剖析文。

文章帶分析的源碼我也掛在我的 github 倉庫上了,有興趣的童鞋可以去下載來看。

閑話不多說,咱們開始深入 FastClick 源碼陣營。

我們知道,注冊一個(gè) FastClick 事件非常簡單,它是這樣的:

if ('addEventListener' in document) {
  document.addEventListener('DOMContentLoaded', function() {
    var fc = FastClick.attach(document.body); //生成實(shí)例
  }, false);
}

所以我們從這里著手,打開源碼看下 FastClick .attach 方法:

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

這里返回了一個(gè) FastClick 實(shí)例,所以咱們拉到前面看看 FastClick 構(gòu)造函數(shù):

function FastClick(layer, options) {
    var oldOnClick;
    options = options || {};
    //定義了一些參數(shù)...
    //如果是屬于不需要處理的元素類型,則直接返回
    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 中會(huì)判斷 event.propagationStopped 是否為真來確保(安卓的onMouse事件)只執(zhí)行一次
          //在 onMouse 事件里會(huì)給 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;
    }
  }

在初始通過 FastClick.notNeeded 方法判斷是否需要做后續(xù)的相關(guān)處理:

//如果是屬于不需要處理的元素類型,則直接返回
    if (FastClick.notNeeded(layer)) {
      return;
    }

我們看下這個(gè) FastClick.notNeeded 都做了哪些判斷:

  //是否沒必要使用到 Fastclick 的檢測
  FastClick.notNeeded = function(layer) {
    var metaViewport;
    var chromeVersion;
    var blackberryVersion;
    var firefoxVersion;

    // 不支持觸摸的設(shè)備
    if (typeof window.ontouchstart === 'undefined') {
      return true;
    }
    // 獲取Chrome版本號(hào),若非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 標(biāo)簽的 chrome 是會(huì)自動(dòng)禁用 300ms 延遲的,所以無需 Fastclick
          if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
            return true;
          }
          // 安卓Chrome 32 及以上版本,若帶有 width=device-width 的 meta 標(biāo)簽也是無需 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 會(huì)禁用雙擊放大,也沒有 300ms 時(shí)延
    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;
  };

基本上都是一些能禁用 300ms 時(shí)延的瀏覽器嗅探,它們都沒必要使用 Fastclick,所以會(huì)返回 true 回構(gòu)造函數(shù)停止下一步執(zhí)行。

由于安卓手Q的 ua 會(huì)被匹配到 /Chrome\/([0-9]+)/,故帶有 'user-scalable=no' meta 標(biāo)簽的安卓手Q頁會(huì)被 FastClick 視為無需處理頁。

這也是為何在安卓手Q里沒有開頭提及問題的原因。

我們繼續(xù)看構(gòu)造函數(shù),它直接給 layer(即body)添加了click、touchstart、touchmove、touchend、touchcancel(若是安卓還有 mouseover、mousedown、mouseup)事件監(jiān)聽:

//安卓則做額外處理
    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);

注意在這段代碼上面還利用了 bind 方法做了處理,這些事件回調(diào)中的 this 都會(huì)變成 Fastclick 實(shí)例上下文。

另外還得留意,onclick 事件以及安卓的額外處理部分都是走的捕獲監(jiān)聽。

咱們分別看看這些事件回調(diào)分別都做了什么。

1. this.onTouchStart

  FastClick.prototype.onTouchStart = function(event) {
    var targetElement, touch, selection;
    // 多指觸控的手勢則忽略
    if (event.targetTouches.length > 1) {
      return true;
    }
    targetElement = this.getTargetElementFromEventTarget(event.target); //一些較老的瀏覽器,target 可能會(huì)是一個(gè)文本節(jié)點(diǎn),得返回其DOM節(jié)點(diǎn)
    touch = event.targetTouches[0];
    if (deviceIsIOS) { //IOS處理
      // 若用戶已經(jīng)選中了一些內(nèi)容(比如選中了一段文本打算復(fù)制),則忽略
      selection = window.getSelection();
      if (selection.rangeCount && !selection.isCollapsed) {
        return true;
      }
      if (!deviceIsIOS4) { //是否IOS4
        //怪異特性處理——若click事件回調(diào)打開了一個(gè)alert/confirm,用戶下一次tap頁面的其它地方時(shí),新的touchstart和touchend
        //事件會(huì)擁有同一個(gè)touch.identifier(新的 touch event 會(huì)跟上一次觸發(fā)alert點(diǎn)擊的 touch event 一樣),
        //為避免將新的event當(dāng)作之前的event導(dǎo)致問題,這里需要禁用事件
        //另外chrome的開發(fā)工具啟用'Emulate touch events'后,iOS UA下的 identifier 會(huì)變成0,所以要做容錯(cuò)避免調(diào)試過程也被禁用事件了
        if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
          event.preventDefault();
          return false;
        }
        this.lastTouchIdentifier = touch.identifier;
        // 如果target是一個(gè)滾動(dòng)容器里的一個(gè)子元素(使用了 -webkit-overflow-scrolling: touch) ,而且滿足:
        // 1) 用戶非??焖俚貪L動(dòng)外層滾動(dòng)容器
        // 2) 用戶通過tap停止住了這個(gè)快速滾動(dòng)
        // 這時(shí)候最后的'touchend'的event.target會(huì)變成用戶最終手指下的那個(gè)元素
        // 所以當(dāng)快速滾動(dòng)開始的時(shí)候,需要做檢查target是否滾動(dòng)容器的子元素,如果是,做個(gè)標(biāo)記
        // 在touchend時(shí)檢查這個(gè)標(biāo)記的值(滾動(dòng)容器的scrolltop)是否改變了,如果是則說明頁面在滾動(dòng)中,需要取消fastclick處理
        this.updateScrollParent(targetElement);
      }
    }
    this.trackingClick = true; //做個(gè)標(biāo)志表示開始追蹤click事件了
    this.trackingClickStart = event.timeStamp; //標(biāo)記下touch事件開始的時(shí)間戳
    this.targetElement = targetElement;
    //標(biāo)記touch起始點(diǎn)的頁面偏移值
    this.touchStartX = touch.pageX;
    this.touchStartY = touch.pageY;
    // this.lastClickTime 是在 touchend 里標(biāo)記的事件時(shí)間戳
    // this.tapDelay 為常量 200 (ms)
    // 此舉用來避免 phantom 的雙擊(200ms內(nèi)快速點(diǎn)了兩次)觸發(fā) click
    // 反正200ms內(nèi)的第二次點(diǎn)擊會(huì)禁止觸發(fā)其默認(rèn)事件
    if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
      event.preventDefault();
    }
    return true;
  };

順道看下這里的 this.updateScrollParent:

  /**
   * 檢查target是否一個(gè)滾動(dòng)容器里的子元素,如果是則給它加個(gè)標(biāo)記
   */
  FastClick.prototype.updateScrollParent = function(targetElement) {
    var scrollParent, parentElement;
    scrollParent = targetElement.fastClickScrollParent;
    if (!scrollParent || !scrollParent.contains(targetElement)) {
      parentElement = targetElement;
      do {
        if (parentElement.scrollHeight > parentElement.offsetHeight) {
          scrollParent = parentElement;
          targetElement.fastClickScrollParent = parentElement;
          break;
        }
        parentElement = parentElement.parentElement;
      } while (parentElement);
    }
    // 給滾動(dòng)容器加個(gè)標(biāo)志fastClickLastScrollTop,值為其當(dāng)前垂直滾動(dòng)偏移
    if (scrollParent) {
      scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
    }
  };

另外要注意的是,在 onTouchStart 里被標(biāo)記為 true 的 this.trackingClick 屬性,都會(huì)在其它事件回調(diào)(比如 ontouchmove )的開頭做檢測,如果沒被賦值過,則直接忽略:

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

當(dāng)然在 ontouchend 事件里會(huì)把它重置為 false。

2. this.onTouchMove

這段代碼量好少:

  FastClick.prototype.onTouchMove = function(event) {
    //不是需要被追蹤click的事件則忽略
    if (!this.trackingClick) {
      return true;
    }
    // 如果target突然改變了,或者用戶其實(shí)是在移動(dòng)手勢而非想要click
    // 則應(yīng)該清掉this.trackingClick和this.targetElement,告訴后面的事件你們也不用處理了
    if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
      this.trackingClick = false;
      this.targetElement = null;
    }
    return true;
  };

看下這里用到的 this.touchHasMoved 原型方法:

  //判斷是否移動(dòng)了
  //this.touchBoundary是常量,值為10
  //如果touch已經(jīng)移動(dòng)了10個(gè)偏移量單位,則應(yīng)當(dāng)作為移動(dòng)事件處理而非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;
  };

3. onTouchEnd

  FastClick.prototype.onTouchEnd = function(event) {
    var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
    if (!this.trackingClick) {
      return true;
    }
    // 避免 phantom 的雙擊(200ms內(nèi)快速點(diǎn)了兩次)觸發(fā) click
    // 我們在 ontouchstart 里已經(jīng)做過一次判斷了(僅僅禁用默認(rèn)事件),這里再做一次判斷
    if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
      this.cancelNextClick = true; //該屬性會(huì)在 onMouse 事件中被判斷,為true則徹底禁用事件和冒泡
      return true;
    }
    //this.tapTimeout是常量,值為700
    //識(shí)別是否為長按事件,如果是(大于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; //標(biāo)記touchend時(shí)間,方便下一次的touchstart做雙擊校驗(yàn)
    trackingClickStart = this.trackingClickStart;
    //重置 this.trackingClick 和 this.trackingClickStart
    this.trackingClick = false;
    this.trackingClickStart = 0;
    // iOS 6.0-7.*版本下有個(gè)問題 —— 如果layer處于transition或scroll過程,event所提供的target是不正確的
    // 所以咱們得重找 targetElement(這里通過 document.elementFromPoint 接口來尋找)
    if (deviceIsIOSWithBadTarget) { //iOS 6.0-7.*版本
      touch = event.changedTouches[0]; //手指離開前的觸點(diǎn)
      // 有些情況下 elementFromPoint 里的參數(shù)是預(yù)期外/不可用的, 所以還得避免 targetElement 為 null
      targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
      // target可能不正確需要重找,但fastClickScrollParent是不會(huì)變的
      targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
    }
    targetTagName = targetElement.tagName.toLowerCase();
    if (targetTagName === 'label') { //是label則激活其指向的組件
      forElement = this.findControl(targetElement);
      if (forElement) {
        this.focus(targetElement);
        //安卓直接返回(無需合成click事件觸發(fā),因?yàn)辄c(diǎn)擊和激活元素不同,不存在點(diǎn)透)
        if (deviceIsAndroid) {
          return false;
        }
        targetElement = forElement;
      }
    } else if (this.needsFocus(targetElement)) { //非label則識(shí)別是否需要focus的元素
      //手勢停留在組件元素時(shí)長超過100ms,則置空this.targetElement并返回
      //(而不是通過調(diào)用this.focus來觸發(fā)其聚焦事件,走的原生的click/focus事件觸發(fā)流程)
      //這也是為何文章開頭提到的問題中,稍微久按一點(diǎn)(超過100ms)textarea是可以把光標(biāo)定位在正確的地方的原因
      //另外iOS下有個(gè)意料之外的bug——如果被點(diǎn)擊的元素所在文檔是在iframe中的,手動(dòng)調(diào)用其focus的話,
      //會(huì)發(fā)現(xiàn)你往其中輸入的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); //立即觸發(fā)其click事件,而無須等待300ms
      //iOS4下的 select 元素不能禁用默認(rèn)事件(要確保它能被穿透),否則不會(huì)打開select目錄
      //有時(shí)候 iOS6/7 下(VoiceOver開啟的情況下)也會(huì)如此
      if (!deviceIsIOS || targetTagName !== 'select') {
        this.targetElement = null;
        event.preventDefault();
      }

      return false;
    }
    if (deviceIsIOS && !deviceIsIOS4) {
      // 滾動(dòng)容器的垂直滾動(dòng)偏移改變了,說明是容器在做滾動(dòng)而非點(diǎn)擊,則忽略
      scrollParent = targetElement.fastClickScrollParent;
      if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
        return true;
      }
    }
    // 查看元素是否無需處理的白名單內(nèi)(比如加了名為“needsclick”的class)
    // 不是白名單的則照舊預(yù)防穿透處理,立即觸發(fā)合成的click事件
    if (!this.needsClick(targetElement)) {
      event.preventDefault();
      this.sendClick(targetElement, event);
    }
    return false;
  };

這段比較長,我們主要看這段:

} else if (this.needsFocus(targetElement)) { //非label則識(shí)別是否需要focus的元素
      //手勢停留在組件元素時(shí)長超過100ms,則置空this.targetElement并返回
      //(而不是通過調(diào)用this.focus來觸發(fā)其聚焦事件,走的原生的click/focus事件觸發(fā)流程)
      //這也是為何文章開頭提到的問題中,稍微久按一點(diǎn)(超過100ms)textarea是可以把光標(biāo)定位在正確的地方的原因
      //另外iOS下有個(gè)意料之外的bug——如果被點(diǎn)擊的元素所在文檔是在iframe中的,手動(dòng)調(diào)用其focus的話,
      //會(huì)發(fā)現(xiàn)你往其中輸入的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); //立即觸發(fā)其click事件,而無須等待300ms

      //iOS4下的 select 元素不能禁用默認(rèn)事件(要確保它能被穿透),否則不會(huì)打開select目錄
      //有時(shí)候 iOS6/7 下(VoiceOver開啟的情況下)也會(huì)如此
      if (!deviceIsIOS || targetTagName !== 'select') {
        this.targetElement = null;
        event.preventDefault();
      }
      return false;
    }

其中 this.needsFocus 用于判斷給定元素是否需要通過合成click事件來模擬聚焦:

  //判斷給定元素是否需要通過合成click事件來模擬聚焦
  FastClick.prototype.needsFocus = function(target) {
    switch (target.nodeName.toLowerCase()) {
      case 'textarea':
        return true;
      case 'select':
        return !deviceIsAndroid; //iOS下的select得走穿透點(diǎn)擊才行
      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);
    }
  };

另外這段說明了為何稍微久按一點(diǎn)(超過100ms)textarea ,我們是可以把光標(biāo)定位在正確的地方(會(huì)繞過后面調(diào)用 this.focus 的方法):

  //手勢停留在組件元素時(shí)長超過100ms,則置空this.targetElement并返回
      //(而不是通過調(diào)用this.focus來觸發(fā)其聚焦事件,走的原生的click/focus事件觸發(fā)流程)
      //這也是為何文章開頭提到的問題中,稍微久按一點(diǎn)(超過100ms)textarea是可以把光標(biāo)定位在正確的地方的原因
      //另外iOS下有個(gè)意料之外的bug——如果被點(diǎn)擊的元素所在文檔是在iframe中的,手動(dòng)調(diào)用其focus的話,
      //會(huì)發(fā)現(xiàn)你往其中輸入的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); //立即觸發(fā)其click事件,而無須等待300ms

所涉及的兩個(gè)原型方法分別為:

⑴ this.focus

  FastClick.prototype.focus = function(targetElement) {
    var length;

    // 組件建議通過setSelectionRange(selectionStart, selectionEnd)來設(shè)定光標(biāo)范圍(注意這樣還沒有聚焦
    // 要等到后面觸發(fā) sendClick 事件才會(huì)聚焦)
    // 另外 iOS7 下有些input元素(比如 date datetime month) 的 selectionStart 和 selectionEnd 特性是沒有整型值的,
    // 導(dǎo)致會(huì)拋出一個(gè)關(guān)于 setSelectionRange 的模糊錯(cuò)誤,它們需要改用 focus 事件觸發(fā)
    if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
      length = targetElement.value.length;
      targetElement.setSelectionRange(length, length);
    } else {
      //直接觸發(fā)其focus事件
      targetElement.focus();
    }
  };

注意,我們點(diǎn)擊 textarea 時(shí)調(diào)用了該方法,它通過 targetElement.setSelectionRange(length, length) 決定了光標(biāo)的位置在內(nèi)容的尾部(但注意,這時(shí)候還沒聚焦?。。。?。

⑵ this.sendClick

真正讓 textarea 聚焦的是這個(gè)方法,它合成了一個(gè) click 方法立刻在textarea元素上觸發(fā)導(dǎo)致聚焦:

  //合成一個(gè)click事件并在指定元素上觸發(fā)
  FastClick.prototype.sendClick = function(targetElement, event) {
    var clickEvent, touch;

    // 在一些安卓機(jī)器中,得讓頁面所存在的 activeElement(聚焦的元素,比如input)失焦,否則合成的click事件將無效
    if (document.activeElement && document.activeElement !== targetElement) {
      document.activeElement.blur();
    }

    touch = event.changedTouches[0];

    // 合成(Synthesise) 一個(gè) click 事件
    // 通過一個(gè)額外屬性確保它能被追蹤(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的內(nèi)部變量,用來識(shí)別click事件是原生還是合成的
    targetElement.dispatchEvent(clickEvent); //立即觸發(fā)其click事件
  };

  FastClick.prototype.determineEventType = function(targetElement) {

    //安卓設(shè)備下 Select 無法通過合成的 click 事件被展開,得改為 mousedown
    if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
      return 'mousedown';
    }

    return 'click';
  };

經(jīng)過這么一折騰,咱們輕點(diǎn) textarea 后,光標(biāo)就自然定位到其內(nèi)容尾部去了。但是這里有個(gè)問題——排在 touchend 后的 focus 事件為啥沒被觸發(fā)呢?

如果 focus 事件能被觸發(fā)的話,那肯定能重新定位光標(biāo)到正確的位置呀。

咱們看下面這段:

  //iOS4下的 select 元素不能禁用默認(rèn)事件(要確保它能被穿透),否則不會(huì)打開select目錄
      //有時(shí)候 iOS6/7 下(VoiceOver開啟的情況下)也會(huì)如此
      if (!deviceIsIOS || targetTagName !== 'select' ) {
        this.targetElement = null;
        event.preventDefault();
      }

通過 preventDefault 的阻擋,textarea 自然再也無法擁抱其 focus 寶寶了~

于是乎,我們在這里做個(gè)改動(dòng)就能修復(fù)這個(gè)問題:

  var _isTextInput = function(){
        return targetTagName === 'textarea' || (targetTagName === 'input' && targetElement.type === 'text');
      };
      
      if ((!deviceIsIOS || targetTagName !== 'select') && !_isTextInput()) {
        this.targetElement = null;
        event.preventDefault();
      }

或者:

if (!deviceIsIOS4 || targetTagName !== 'select') {
  this.targetElement = null;
  //給textarea加上“needsclick”的class
  if((!/\bneedsclick\b/).test(targetElement.className)){
    event.preventDefault(); 
  }
}

這里要吐槽下的是,F(xiàn)astclick 把 this.needsClick 放到了 ontouchEnd 末尾去執(zhí)行,才導(dǎo)致前面說的加上了“needsclick”類名也無效的問題。

雖然問題原因找到也解決了,但咱們還是繼續(xù)看剩下的部分吧。

4. onMouse 和 onClick

  //用于決定是否允許穿透事件(觸發(fā)layer的click默認(rèn)事件)
  FastClick.prototype.onMouse = function(event) {
    // touch事件一直沒觸發(fā)
    if (!this.targetElement) {
      return true;
    }
    if (event.forwardedTouchEvent) { //觸發(fā)的click事件是合成的
      return true;
    }
    // 編程派生的事件所對應(yīng)元素事件可以被允許
    // 確保其沒執(zhí)行過 preventDefault 方法(event.cancelable 不為 true)即可
    if (!event.cancelable) {
      return true;
    }
    // 需要做預(yù)防穿透處理的元素,或者做了快速(200ms)雙擊的情況
    if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
      //停止當(dāng)前默認(rèn)事件和冒泡
      if (event.stopImmediatePropagation) {
        event.stopImmediatePropagation();
      } else {
        // 不支持 stopImmediatePropagation 的設(shè)備(比如Android 2)做標(biāo)記,
        // 確保該事件回調(diào)不會(huì)執(zhí)行(見126行)
        event.propagationStopped = true;
      }
      // 取消事件和冒泡
      event.stopPropagation();
      event.preventDefault();
      return false;
    }
    //允許穿透
    return true;
  };
  //click事件常規(guī)都是touch事件衍生來的,也排在touch后面觸發(fā)。
  //對于那些我們在touch事件過程沒有禁用掉默認(rèn)事件的event來說,我們還需要在click的捕獲階段進(jìn)一步
  //做判斷決定是否要禁掉點(diǎn)擊事件(防穿透)
  FastClick.prototype.onClick = function(event) {
    var permitted;
    // 如果還有 trackingClick 存在,可能是某些UI事件阻塞了touchEnd 的執(zhí)行
    if (this.trackingClick) {
      this.targetElement = null;
      this.trackingClick = false;
      return true;
    }
    // 依舊是對 iOS 怪異行為的處理 —— 如果用戶點(diǎn)擊了iOS模擬器里某個(gè)表單中的一個(gè)submit元素
    // 或者點(diǎn)擊了彈出來的鍵盤里的“Go”按鈕,會(huì)觸發(fā)一個(gè)“偽”click事件(target是一個(gè)submit-type的input元素)
    if (event.target.type === 'submit' && event.detail === 0) {
      return true;
    }
    permitted = this.onMouse(event);
    if (!permitted) { //如果點(diǎn)擊是被允許的,將this.targetElement置空可以確保onMouse事件里不會(huì)阻止默認(rèn)事件
      this.targetElement = null;
    }
    //沒有多大意義
    return permitted;
  };

  //銷毀Fastclick所注冊的監(jiān)聽事件。是給外部實(shí)例去調(diào)用的
  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);
  };

常規(guī)需要阻斷點(diǎn)擊事件的操作,我們在 touch 監(jiān)聽事件回調(diào)中已經(jīng)做了處理,這里主要是針對那些 touch 過程(有些設(shè)備甚至可能并沒有touch事件觸發(fā))沒有禁用默認(rèn)事件的 event 做進(jìn)一步處理,從而決定是否觸發(fā)原生的 click 事件(如果禁止是在 onMouse 方法里做的處理)。

以上是“FastClick的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI