超小手势库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阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270