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

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

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

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

核心庫:

/* AlloyFinger v0.1.10
 * 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,會移除新頁面元素剛剛綁定的事件。導致新頁面綁定事件失敗。
已向官方提交了issue。鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372