超小手勢庫alloyfinger及其vue版實現深刻解析

alloyfinger是一款很是輕量的開源手勢庫,因爲其輕量、基於原生js等特性被普遍使用。關於其原理,它的官方團隊解析的很是詳細——傳送門。相信學太高數的人看起來應該不難,這裏不深刻解析了。html

其核心代碼只有300多行,完成了14個手勢,其手勢並非瀏覽器原生的事件,而是經過監聽touchstart、touchmove、touchend、touchcancel四個原生瀏覽器事件hack出來的手勢,故其用法與原生可能有些不一樣。好比阻止默認事件、阻止冒泡,不能像原生事件那樣用。vue

官方代碼除了alloyfinger的核心庫外還有react、vue的實現。在這裏只對核心庫即vue版本的解析。react

核心庫:android

/* AlloyFinger v0.1.7 * By dntzhang * Github: https://github.com/AlloyTeam/AlloyFinger * Note By keenjaan * Github: https://github.com/keenjaan */
; (function () {
    // 計算距離和角度等的數學公式

    // 根據兩邊的長度求直角三角形斜邊長度(主要用於求兩點距離)
    function getLen(v) {
        return Math.sqrt(v.x * v.x + v.y * v.y);
    }
    // 主要用於計算兩次手勢狀態間的夾角的輔助函數
    function dot(v1, v2) {
        return v1.x * v2.x + v1.y * v2.y;
    }
    // 計算兩次手勢狀態間的夾角
    function getAngle(v1, v2) {
        var mr = getLen(v1) * getLen(v2);
        if (mr === 0) return 0;
        var r = dot(v1, v2) / mr;
        if (r > 1) r = 1;
        return Math.acos(r);
    }
    // 計算夾角的旋轉方向,(逆時針大於0,順時針小於0)
    function cross(v1, v2) {
        return v1.x * v2.y - v2.x * v1.y;
    }
    // 將角度轉換爲弧度,而且絕對值
    function getRotateAngle(v1, v2) {
        var angle = getAngle(v1, v2);
        if (cross(v1, v2) > 0) {
            angle *= -1;
        }
        return angle * 180 / Math.PI;
    }
    // 用於處理手勢監聽函數的構造函數
    var HandlerAdmin = function(el) {
        this.handlers = []; // 監聽函數列表
        this.el = el;       // 監聽元素
    };
    // 構造函數的添加監聽函數的方法
    HandlerAdmin.prototype.add = function(handler) {
        this.handlers.push(handler);
    }
    // 構造函數的刪除監聽函數的方法
    HandlerAdmin.prototype.del = function(handler) {
        if(!handler) this.handlers = []; // handler爲假值時,表明清空監聽函數列表
        for(var i=this.handlers.length; i>=0; i--) {
            if(this.handlers[i] === handler) {
                this.handlers.splice(i, 1);
            }
        }
    }
    // 觸發用戶事件監聽回調函數
    HandlerAdmin.prototype.dispatch = function() {
        for(var i=0,len=this.handlers.length; i<len; i++) {
            var handler = this.handlers[i];
            if(typeof handler === 'function') handler.apply(this.el, arguments);
        }
    }
    // 實例化處理監聽函數的對象
    function wrapFunc(el, handler) {
        var handlerAdmin = new HandlerAdmin(el);
        handlerAdmin.add(handler);  // 添加監聽函數
        return handlerAdmin; // 返回實例
    }
    // 手勢的構造函數
    var AlloyFinger = function (el, option) {
      
        this.element = typeof el == 'string' ? document.querySelector(el) : el; // 綁定事件的元素

        // 綁定原型上start, move, end, cancel函數的this對象爲 AlloyFinger實例
        this.start = this.start.bind(this);
        this.move = this.move.bind(this);
        this.end = this.end.bind(this);
        this.cancel = this.cancel.bind(this);

        // 綁定原生的 touchstart, touchmove, touchend, touchcancel事件。
        this.element.addEventListener("touchstart", this.start, false);
        this.element.addEventListener("touchmove", this.move, false);
        this.element.addEventListener("touchend", this.end, false);
        this.element.addEventListener("touchcancel", this.cancel, false);
      
		// 保存當有兩個手指以上時,兩個手指間橫縱座標的差值,用於計算兩點距離
        this.preV = { x: null, y: null };   
        this.pinchStartLen = null;  // 兩個手指間的距離
        this.zoom = 1;              // 初始縮放比例
        this.isDoubleTap = false;   // 是否雙擊

        var noop = function () { }; // 空函數,沒有綁定事件時,傳入的函數

        // 對14種手勢,分別實例化監聽函數對象,根據option的值添加相關監聽函數,沒有就添加空函數。
        this.rotate = wrapFunc(this.element, option.rotate || noop);
        this.touchStart = wrapFunc(this.element, option.touchStart || noop);
        this.multipointStart = wrapFunc(this.element, option.multipointStart || noop);
        this.multipointEnd = wrapFunc(this.element, option.multipointEnd || noop);
        this.pinch = wrapFunc(this.element, option.pinch || noop);
        this.swipe = wrapFunc(this.element, option.swipe || noop);
        this.tap = wrapFunc(this.element, option.tap || noop);
        this.doubleTap = wrapFunc(this.element, option.doubleTap || noop);
        this.longTap = wrapFunc(this.element, option.longTap || noop);
        this.singleTap = wrapFunc(this.element, option.singleTap || noop);
        this.pressMove = wrapFunc(this.element, option.pressMove || noop);
        this.touchMove = wrapFunc(this.element, option.touchMove || noop);
        this.touchEnd = wrapFunc(this.element, option.touchEnd || noop);
        this.touchCancel = wrapFunc(this.element, option.touchCancel || noop);

        this.delta = null;  // 用於判斷是不是雙擊的時間戳
        this.last = null;   // 記錄時間戳的變量
        this.now = null;    // 記錄時間戳的變量
        this.tapTimeout = null;         //tap事件執行的定時器
        this.singleTapTimeout = null;   // singleTap執行的定時器
        this.longTapTimeout = null;     // longTap執行的定時器
        this.swipeTimeout = null;       // swipe執行的定時器
        this.x1 = this.x2 = this.y1 = this.y2 = null;   // start時手指的座標x1, y1, move時手指的座標x2, y2
        this.preTapPosition = { x: null, y: null };     // 記住start時,手指的座標
    };

    AlloyFinger.prototype = {
        start: function (evt) {
            if (!evt.touches) return;   // touches手指列表,沒有就return
            this.now = Date.now();      // 記錄當前事件點
            this.x1 = evt.touches[0].pageX;     // 第一個手指x座標
            this.y1 = evt.touches[0].pageY;     // 第一個手指y座標
            this.delta = this.now - (this.last || this.now);    // 時間戳
            this.touchStart.dispatch(evt);      // 觸發touchStart事件
            if (this.preTapPosition.x !== null) {   
            // 不是第一次觸摸屏幕時,比較兩次觸摸時間間隔,兩次觸摸間隔小於250ms,觸摸點的距離小於30px時記爲雙擊。
                this.isDoubleTap = (this.delta > 0 && this.delta <= 250 && Math.abs(this.preTapPosition.x - this.x1) < 30 && Math.abs(this.preTapPosition.y - this.y1) < 30);
            }
            this.preTapPosition.x = this.x1;    // 將這次的觸摸座標保存到preTapPosition。
            this.preTapPosition.y = this.y1;
            this.last = this.now;               // 記錄本次觸摸時間點
            var preV = this.preV,               // 獲取記錄的兩點座標差值
                len = evt.touches.length;       // 手指個數
            if (len > 1) {                      // 手指個數大於1
                this._cancelLongTap();          // 取消longTap定時器
                this._cancelSingleTap();        // 取消singleTap定時器
                var v = { x: evt.touches[1].pageX - this.x1, y: evt.touches[1].pageY - this.y1 };
                // 計算兩個手指間橫縱座標差,並保存到prev對象中,也保存到this.preV中。
                preV.x = v.x;
                preV.y = v.y;
                this.pinchStartLen = getLen(preV);  // 計算兩個手指的間距
                this.multipointStart.dispatch(evt); // 觸發multipointStart事件
            }
            // 開啓longTap事件定時器,若是750ms內定時器沒有被清除則觸發longTap事件。
            this.longTapTimeout = setTimeout(function () {
                this.longTap.dispatch(evt);
            }.bind(this), 750);
        },
        move: function (evt) {
            if (!evt.touches) return;
            var preV = this.preV,   // start方法中保存的兩點橫縱座標差值。
                len = evt.touches.length,   // 手指個數
                currentX = evt.touches[0].pageX,    // 第一個手指的x座標
                currentY = evt.touches[0].pageY;    // 第一個手指的y座標
            this.isDoubleTap = false;               // 移動了就不能是雙擊事件了
            if (len > 1) {
                // 獲取當前兩點橫縱座標的差值,保存到v對象中。
                var v = { x: evt.touches[1].pageX - currentX, y: evt.touches[1].pageY - currentY };
                // start保存的preV不爲空,pinchStartLen大於0
                if (preV.x !== null) {
                    if (this.pinchStartLen > 0) {
                        // 當前兩點的距離除以start中兩點距離,求出縮放比,掛載到evt對象中
                        evt.zoom = getLen(v) / this.pinchStartLen;  
                        this.pinch.dispatch(evt);   // 觸發pinch事件
                    }

                    evt.angle = getRotateAngle(v, preV);    // 計算旋轉的角度,掛載到evt對象中
                    this.rotate.dispatch(evt);      // 觸發rotate事件
                }
                preV.x = v.x;   // 將move中的兩個手指的橫縱座標差值賦值給preV,同時也改變了this.preV
                preV.y = v.y;
            } else {
                // 出列一根手指的pressMove手勢

                // 第一次觸發move時,this.x2爲null,move執行完會有給this.x2賦值。
                if (this.x2 !== null) {
                    // 用本次的move座標減去上一次move座標,獲得x,y方向move距離。
                    evt.deltaX = currentX - this.x2;
                    evt.deltaY = currentY - this.y2;

                } else {
                    // 第一次執行move,因此移動距離爲0,將evt.deltaX,evt.deltaY賦值爲0.
                    evt.deltaX = 0;
                    evt.deltaY = 0;
                }
                // 觸發pressMove事件
                this.pressMove.dispatch(evt);
            }
            // 觸發touchMove事件,掛載不一樣的屬性給evt對象拋給用戶
            this.touchMove.dispatch(evt);

            // 取消長按定時器,750ms內能夠阻止長按事件。
            this._cancelLongTap();
            this.x2 = currentX;     // 記錄當前第一個手指座標
            this.y2 = currentY;
            if (len > 1) {
                evt.preventDefault();   // 兩個手指以上阻止默認事件
            }
        },
        end: function (evt) {
            if (!evt.changedTouches) return;
            // 取消長按定時器,750ms內會阻止長按事件
            this._cancelLongTap();   
            var self = this;    // 保存當前this對象。
            // 若是當前留下來的手指數小於2,觸發multipointEnd事件
            if (evt.touches.length < 2) {
                this.multipointEnd.dispatch(evt);
            }

            // this.x2或this.y2存在表明觸發了move事件。
            // Math.abs(this.x1 - this.x2)表明在x方向移動的距離。
            // 故就是在x方向或y方向移動的距離大於30px時則觸發swipe事件
            if ((this.x2 && Math.abs(this.x1 - this.x2) > 30) ||
                (this.y2 && Math.abs(this.y1 - this.y2) > 30)) {
                // 計算swipe的方向並寫入evt對象。
                evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2);
                this.swipeTimeout = setTimeout(function () {
                    self.swipe.dispatch(evt);   // 異步觸發swipe事件

                }, 0)
            } else {
                this.tapTimeout = setTimeout(function () {
                    self.tap.dispatch(evt); // 異步觸發tap事件
                    // trigger double tap immediately
                    if (self.isDoubleTap) { // start方法中計算的知足雙擊條件時
                        self.doubleTap.dispatch(evt);   // 觸發雙擊事件
                        clearTimeout(self.singleTapTimeout);    // 清楚singleTap事件定時器
                        self.isDoubleTap = false;   // 重置雙擊條件
                    }
                }, 0)

                if (!self.isDoubleTap) {    // 若是不知足雙擊條件
                    self.singleTapTimeout = setTimeout(function () {
                        self.singleTap.dispatch(evt);   // 觸發singleTap事件
                    }, 250);
                }
            }

            this.touchEnd.dispatch(evt);    // 觸發touchEnd事件
            // end結束後重置相關的變量
            this.preV.x = 0;
            this.preV.y = 0;
            this.zoom = 1;
            this.pinchStartLen = null;
            this.x1 = this.x2 = this.y1 = this.y2 = null;
        },
        cancel: function (evt) {
       
            // 關閉全部定時器
            clearTimeout(this.singleTapTimeout);
            clearTimeout(this.tapTimeout);
            clearTimeout(this.longTapTimeout);
            clearTimeout(this.swipeTimeout);
            this.touchCancel.dispatch(evt);
        },
        _cancelLongTap: function () {
            clearTimeout(this.longTapTimeout); // 關閉longTap定時器
        },
        _cancelSingleTap: function () {
            clearTimeout(this.singleTapTimeout); // 關閉singleTap定時器
        },
        _swipeDirection: function (x1, x2, y1, y2) {
            // 判斷swipe方向
            return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
        },
        // 給14中手勢中一種手勢添加監聽函數
        on: function(evt, handler) {
            if(this[evt]) { // 事件名在這14中之中,才添加函數到監聽事件中
                this[evt].add(handler);
            }
        },
        // 給14中手勢中一種手勢移除監聽函數
        off: function(evt, handler) {
            if(this[evt]) { // 事件名在這14中之中,才移除相應監聽函數
                this[evt].del(handler);
            }
        },
        // 清空,重置全部數據
        destroy: function() {
            // 關閉全部定時器
            if(this.singleTapTimeout) clearTimeout(this.singleTapTimeout);
            if(this.tapTimeout) clearTimeout(this.tapTimeout);
            if(this.longTapTimeout) clearTimeout(this.longTapTimeout);
            if(this.swipeTimeout) clearTimeout(this.swipeTimeout);
            // 移除touch的四個事件
            this.element.removeEventListener("touchstart", this.start);
            this.element.removeEventListener("touchmove", this.move);
            this.element.removeEventListener("touchend", this.end);
            this.element.removeEventListener("touchcancel", this.cancel);
            // 清除全部手勢的監聽函數
            this.rotate.del();
            this.touchStart.del();
            this.multipointStart.del();
            this.multipointEnd.del();
            this.pinch.del();
            this.swipe.del();
            this.tap.del();
            this.doubleTap.del();
            this.longTap.del();
            this.singleTap.del();
            this.pressMove.del();
            this.touchMove.del();
            this.touchEnd.del();
            this.touchCancel.del();
            // 重置全部變量
            this.preV = this.pinchStartLen = this.zoom = this.isDoubleTap = this.delta = this.last = this.now = this.tapTimeout = this.singleTapTimeout = this.longTapTimeout = this.swipeTimeout = this.x1 = this.x2 = this.y1 = this.y2 = this.preTapPosition = this.rotate = this.touchStart = this.multipointStart = this.multipointEnd = this.pinch = this.swipe = this.tap = this.doubleTap = this.singleTap = this.pressMove = this.touchMove = this.touchEnd = this.touchCancel = null;

            return null;
        }
    };
    // 若是當前環境支持module,exports等es6語法,則導出AlloyFingerPlugin模塊
    if (typeof module !== 'undefined' && typeof exports === 'object') {
        module.exports = AlloyFinger;
    } else {  // 不然將AlloyFingerPlugin註冊到全局對象
        window.AlloyFinger = AlloyFinger;
    }
})();
複製代碼

vue 版本代碼:ios

/* AlloyFinger v0.1.0 for Vue * By june01 * Github: https://github.com/AlloyTeam/AlloyFinger * Note By keenjaan * Github: https://github.com/keenjaan */

; (function() {

  var AlloyFingerPlugin = {
    // 用於vue掛載指令的install函數
    install: function(Vue, options) {
      // options掛載指令時傳遞的參數
      options = options || {};
      // AlloyFinger全局中獲取,沒有就讀取options中獲取。
      var AlloyFinger = window.AlloyFinger || options.AlloyFinger;
      // 判斷vue的版本
      var isVue2 = !!(Vue.version.substr(0,1) == 2);
      // 獲取不到AlloyFinger拋出異常
      if(!AlloyFinger) {
        throw new Error('you need include the AlloyFinger!');
      }
      // 14中手勢命名
      var EVENTMAP = {
        'touch-start': 'touchStart',
        'touch-move': 'touchMove',
        'touch-end': 'touchEnd',
        'touch-cancel': 'touchCancel',
        'multipoint-start': 'multipointStart',
        'multipoint-end': 'multipointEnd',
        'tap': 'tap',
        'double-tap': 'doubleTap',
        'long-tap': 'longTap',
        'single-tap': 'singleTap',
        'rotate': 'rotate',
        'pinch': 'pinch',
        'press-move': 'pressMove',
        'swipe': 'swipe'
      };
      // 記錄元素添加監聽事件的數組。
      var CACHE = [];
      // 建立空對象,用於存放vue自定義指令directive的參數對象
      var directiveOpts = {};

      // 獲取某個元素在CACHE中是否存在,存在返回index,不存在返回null
      var getElemCacheIndex = function(elem) {
        for(var i=0,len=CACHE.length; i<len; i++) {
          if(CACHE[i].elem === elem) {
            return i;
          }
        }
        return null;
      };

      // 綁定或解綁事件監聽函數
      var doOnOrOff = function(cacheObj, options) {
        var eventName = options.eventName;  // 事件名
        var elem = options.elem;            // 監聽元素
        var func = options.func;            // 監聽函數
        var oldFunc = options.oldFunc;      // dom更新時,舊的監聽函數
        // 若是給該元素添加過事件
        if(cacheObj && cacheObj.alloyFinger) {
          // 若是是dom更新觸發的,不是初始化綁定事件,即oldFunc存在,就解綁上一次綁定的函數oldFunc。
          if(cacheObj.alloyFinger.off && oldFunc) cacheObj.alloyFinger.off(eventName, oldFunc);
          // 若是func存在,不論是初始化仍是dom更新,都綁定func
          if(cacheObj.alloyFinger.on && func) cacheObj.alloyFinger.on(eventName, func);
        } else {
          // 若是沒有給該元素添加過事件
          options = {};   // 建立空對象
          options[eventName] = func;  // 添加監聽事件的監聽函數

          // 向CACHE中添加監聽元素及其監聽的事件和函數
          CACHE.push({
            elem: elem,
            alloyFinger: new AlloyFinger(elem, options) // 初始化AlloyFinger綁定相關事件
          });
        }
      };

      // vue 自定義指令的初始化函數
      var doBindEvent = function(elem, binding) {
        var func = binding.value;       // 監聽函數
        var oldFunc = binding.oldValue; // 舊的監聽函數
        var eventName = binding.arg;    // 監聽的事件名
        eventName = EVENTMAP[eventName];    // 將事件名轉換爲駝峯法
        var cacheObj = CACHE[getElemCacheIndex(elem)];  // 獲取某個元素是否添加過事件監聽,添加到CACHE。
        // 觸發事件監聽函數的綁定或移除
        doOnOrOff(cacheObj, {
          elem: elem,
          func: func,
          oldFunc: oldFunc,
          eventName: eventName
        });
      };

      // 移除事件監聽函數
      var doUnbindEvent = function(elem) {
        var index = getElemCacheIndex(elem);  // 在CACHE中獲取elem的index值
        if(!isNaN(index)) { // 若是元素在CACHE中存在
          var delArr = CACHE.splice(index, 1);  // 刪除該條監聽事件
          if(delArr.length && delArr[0] && delArr[0].alloyFinger.destroy) {
            delArr[0].alloyFinger.destroy();  // 重置手勢alloyFinger對象,中止全部定時器,移除全部監聽函數,清空全部變量。
          }
        } 
      };
      // 判斷vue版本
      if(isVue2) {  // vue2
        // directive參數
        directiveOpts = {
          bind: doBindEvent,
          update: doBindEvent,
          unbind: doUnbindEvent
        };
      } else {  // vue1
        // vue1.xx
        directiveOpts = {
          update: function(newValue, oldValue) {
            var binding = {
              value: newValue,
              oldValue: oldValue,
              arg: this.arg
            };

            var elem = this.el;

            doBindEvent.call(this, elem, binding);
          },
          unbind: function() {
            var elem = this.el;

            doUnbindEvent.call(this, elem);
          }
        }
      }

      // definition
      Vue.directive('finger', directiveOpts); // 綁定自定義指令finger
    }
  }

  // 若是當前環境支持module,exports等es6語法,則導出AlloyFingerPlugin模塊
  if(typeof module !== 'undefined' && typeof exports === 'object') {
    module.exports = AlloyFingerPlugin;
  } else { // 不然將AlloyFingerPlugin註冊到全局對象
    window.AlloyFingerVue = AlloyFingerPlugin;
  }

})();
複製代碼

上面是整個代碼解析,其中有幾個問題點:git

一、長按是否須要取消tap、swipe、touchend、singleTap、doubleTap等end裏面的全部事件。es6

若是要取消end裏的全部事件,就要添加一個字段isLongTap, 在觸發longTap事件時設置爲true。在end裏判斷isLongTap的值,若是爲true則return掉,阻止end裏的全部事件,並將isLongTap重置爲falsegithub

二、swipe事件和doubleTap的界定,源碼中對swipe與tap的區別是move的距離,當move的距離在x、y方向上都小於等於30px時就爲tap事件,大於30px時就爲swipe事件。doubleTap也同樣,兩次點擊的距離在x、y方向上都小於等於30px,其界定的30px是設置了以下代碼:vue-router

<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
複製代碼

即設置頁面寬度爲設備的理想視口。在個人實際項目中若是進行如上設置,30px這個值可能有點大,會致使想觸發swipe事件結果變成了tap事件。至於到底多少,大家能夠試一下效果,找到符合大家團隊的分界值。數組

還有就是在實際的移動項目中,咱們可能並不會這樣設置你的視口,好比淘寶團隊的flexible適配。其ios端對頁面視口進行了縮放,android端都是用的理想視口(沒有縮放視口),這樣就形成30px對應到屏幕的滑動距離不一樣。在ios端滑動距離較小就能觸發swipe事件。這種狀況下就不能直接使用,要結合你的移動端適配庫,要對alloyfinger源碼作調整。

關於移動端適配能夠查看個人這篇文章 傳送門

方法一:在alloyfinger源碼中直接讀取viewport的縮放,對於不一樣適配機型設置不一樣的修正值,使得在全部機型上觸發swipe事件,手指移動的距離相同。

方法二:是對於vue版本的實現,經過vue的自定義指令,在掛在指令時,動態的經過參數傳進去。

Vue.use(AlloyFingerVue, option)	// 經過參數傳進去。
複製代碼

在AlloyFingerPlugin的install函數中獲取option對象,再將option對象注入到alloyfinger對象中,在alloyfinger中再對swipe的分界值進行修正。 具體實現方案我源碼中已實現,註釋寫的很清楚,不懂能夠問我,源碼連接見文章結尾。

三、阻止冒泡,由於其事件除了touchstart、touchmove、touchend、touchcancel四個原生事件外,其它都是hack的,因此並不能像原生事件那樣在監聽函數中寫阻止冒泡。須要在相應的原生事件中阻止冒泡。在vue版本中能夠經過註冊指令時,傳入參數來阻止冒泡。如:

v-finger:tap.stoppropagation
複製代碼

在doOnOrOff函數中能夠經過modifiers字段讀取到stoppropagation字段,再將stoppropagation字段註冊到alloyfinger對象中。在alloyfinger對象對去該字段來判斷是否須要阻止冒泡。

優勢: 阻止冒泡很是方便,在綁定事件時加一個修飾符便可。

缺點:一旦阻止了冒泡,該元素上全部的事件都阻止了冒泡,若是某一事件須要冒泡,還需特殊處理。

針對以上三點,在官方版本進行了修改。源碼請見 傳送門


官方項目vue版本bug

最近在項目中遇到了個問題,有些頁面按鈕綁定事件失敗。最後找到了問題,官方的vue版本適配有bug。

當使用vue-router切換路由時,上一個頁面銷燬時,全部綁定事件的元素都會觸發doUnbindEvent函數,當一個元素綁定多個事件時,doUnbindEvent函數會觸發屢次。對於一個元素以下:

<div v-finger:tap="tapFunc" v-finger:long-tap="longTapFunc">按鈕</div>
複製代碼

doUnbindEvent函數:

var doUnbindEvent = function(elem) {
      var index = getElemCacheIndex(elem);
    
      if ( index ) {
        return true;
      }
      if(!isNaN(index)) {
        var delArr = CACHE.splice(index, 1);
        if(delArr.length && delArr[0] && delArr[0].alloyFinger.destroy) {
          delArr[0].alloyFinger.destroy();
        }
      }
    };
複製代碼

第一次觸發doUnbindEvent函數, index必定能返回一個number類型數字,會從CACHE中刪除該元素。

當第二次觸發doUnbindEvent時,因爲該元素已被刪除因此index會返回null,而if條件並不能攔截null這個值,

if(!isNaN(index)) {
      //
    }
    故:
    delArr = CACHE.splice(index, 1) = CACHE.splice(null, 1) = CACHE.splice(0, 1);
複製代碼

變成了始終截取CACHE數組中第一個元素。

而當路由切換時,上一個頁面觸發doUnbindEvent函數,新頁面觸發doBindEvent函數,而這二者是同時觸發,致使一邊向CACHE數組中添加綁定元素,一邊從CACHE數組中移除元素。當一個元素綁定多個事件時,存在index爲null,會移除新頁面元素剛剛綁定的事件。致使新頁面綁定事件失敗。

相關文章
相關標籤/搜索