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

keenjaan發表於2017-10-11

alloyfinger是一款非常輕量的開源手勢庫,由於其輕量、基於原生js等特性被廣泛使用。關於其原理,它的官方團隊解析的非常詳細——傳送門。相信學過高數的人看起來應該不難,這裡不深入解析了。

其核心程式碼只有300多行,完成了14個手勢,其手勢並不是瀏覽器原生的事件,而是通過監聽touchstart、touchmove、touchend、touchcancel四個原生瀏覽器事件hack出來的手勢,故其用法與原生可能有些不同。比如阻止預設事件、阻止冒泡,不能像原生事件那樣用。

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

核心庫:

/* 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 版本程式碼:

/* 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;
  }

})();
複製程式碼

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

1、長按是否需要取消tap、swipe、touchend、singleTap、doubleTap等end裡面的所有事件。

如果要取消end裡的所有事件,就要新增一個欄位isLongTap, 在觸發longTap事件時設定為true。在end裡判斷isLongTap的值,如果為true則return掉,阻止end裡的所有事件,並將isLongTap重置為false

2、swipe事件和doubleTap的界定,原始碼中對swipe與tap的區別是move的距離,當move的距離在x、y方向上都小於等於30px時就為tap事件,大於30px時就為swipe事件。doubleTap也一樣,兩次點選的距離在x、y方向上都小於等於30px,其界定的30px是設定瞭如下程式碼:

<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的分界值進行修正。 具體實現方案我原始碼中已實現,註釋寫的很清楚,不懂可以問我,原始碼連結見文章結尾。

3、阻止冒泡,因為其事件除了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,會移除新頁面元素剛剛繫結的事件。導致新頁面繫結事件失敗。

相關文章