欢迎关注微信公众号:chave-cn

手持设备点击响应速度,鼠标事件与touch事件的那些事

前言

现在一直在做移动端的开发,这次将单页应用的网页内嵌入了app,于是老大反映了一个问题:
app应用点击响应慢!
我开始不以为然,于是拿着网页版的试了试,好像确实有一定延迟,于是开始了研究,最后选择了touch取代鼠标事件

但是,touch事件取代mouse事件,还是有一定问题的,据说网上问题很多,因为两者之间还是有一定差异
而且如果完全使用touch事件,对自动化测试的同事来说,他们的系统根本不支持touch事件,再者我们平时网页开发也不方便
所以,了解鼠标事件与touch事件的区别,探讨鼠标事件与touch事件的兼容也是有必要的,于是我们开始今天的学习吧
PS:这里使用zepto框架,懒得自己搞了......

事件差异

鼠标事件

首先,我们来看看鼠标事件相关吧:

var startTime;
var log = function (msg) {
    console.log(new Date().getTime() - startTime);
    console.log(msg);
};
var mouseDown = function () {
    startTime = new Date().getTime();
    log('mouseDown');
};
var mouseClick = function () {
    log('mouseClick');
};
var mouseUp = function () {
    log('mouseUp');
};
 
document.addEventListener('mousedown', mouseDown);
document.addEventListener('click', mouseClick);
document.addEventListener('mouseup', mouseUp);

从这里看到了,鼠标顺序是有mousedown -> click -> mouseup 的顺序,其时间差也出来了

touch事件

然后我们看看touch事件

没有click

touch包含三个事件,touchstart、touchmove、touchend,并没有click事件,所以click事件需要自己模拟,这个我们后面来看看

var startTime;
var log = function (msg) {
    console.log(new Date().getTime() - startTime);
    console.log(msg);
};
var touchStart = function () {
    startTime = new Date().getTime();
    log('touchStart');
};

var touchEnd = function () {
    log('touchEnd');
};

document.addEventListener('touchstart', touchStart);
document.addEventListener('touchend', touchEnd);

在chrome开启touch事件的情况下,可以看到这个结果

 

 

混合事件

现在我们在手机上同时触发两者事件看看区别,这里代码做一定修改

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script id="others_zepto_10rc1" type="text/javascript" class="library" src="http://sandbox.runjs.cn/js/sandbox/other/zepto.min.js"></script>
</head>
<body>
    <div id="d" style="width: 100px; height: 100px; border: 1px solid black;">
    </div>
</body>
<script type="text/javascript">
    var startTime;
    var log = function (msg) {
        var div = $('<div></div>');
        div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg)
        $('body').append(div);

    };
    var touchStart = function () {
        startTime = new Date().getTime();
        log('touchStart');
    };
    var touchEnd = function () {
        log('touchEnd');

    };
    var mouseDown = function () {
        log('mouseDown');
    };
    var mouseClick = function () {
        log('mouseClick');
    };
    var mouseUp = function () {
        log('mouseUp');

    };
    var d = $('#d');
    d.bind('mousedown', mouseDown);
    d.bind('click', mouseClick);
    d.bind('mouseup', mouseUp);
    d.bind('touchstart', touchStart);
    d.bind('touchend', touchEnd);
</script>
</html>

测试地址

http://sandbox.runjs.cn/show/ey54cgqf

此处手机与电脑有非常大的区别!!!

结论

不要同时给document绑定鼠标与touch事件

document.addEventListener('mousedown', mouseDown);
document.addEventListener('click', mouseClick);
document.addEventListener('mouseup', mouseUp);
document.addEventListener('touchstart', touchStart);
document.addEventListener('touchend', touchEnd);

这个样子,在手机上不会触发click事件,click事件要绑定到具体元素

PS:此处的原因我就不去研究了,如果您知道为什么,请留言

手机上mousedown本来响应就慢

经过测试,电脑上touch与click事件的差距不大,但是手机上,当我们手触碰屏幕时,要过300ms左右才会触发mousedown事件

所以click事件在手机上响应就是慢一拍

数据说明

可以看到,在手机上使用click事件其实对用户体验并不好,所以我们可能会逐步使用touch事件

参数差异

现在,我们来看看鼠标与touch事件的参数差异

var startTime;
var log = function (msg, e) {
    console.log(e);
    var div = $('<div></div>');
    div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg)
    $('body').append(div);

};
var touchStart = function (e) {
    startTime = new Date().getTime();
    log('touchStart', e);
};
var touchEnd = function (e) {
    log('touchEnd', e);

};
var mouseDown = function (e) {
    log('mouseDown', e);
};
var mouseClick = function (e) {
    log('mouseClick', e);
};
var mouseUp = function (e) {
    log('mouseUp', e);

};
var d = $('#d');
d.bind('mousedown', mouseDown);
d.bind('click', mouseClick);
d.bind('mouseup', mouseUp);
d.bind('touchstart', touchStart);
d.bind('touchend', touchEnd);

事件参数(touchstart/mouseup)

我们来看几个关键的地方:

changedTouches/touches/targetTouches

touches:为屏幕上所有手指的信息

PS:因为手机屏幕支持多点触屏,所以这里的参数就与手机有所不同

targetTouches:手指在目标区域的手指信息

changedTouches:最近一次触发该事件的手指信息

比如两个手指同时触发事件,2个手指都在区域内,则容量为2,如果是先后离开的的话,就会先触发一次再触发一次,这里的length就是1,只统计最新的

PS:一般changedTouches的length都是1

touchend时,touches与targetTouches信息会被删除,changedTouches保存的最后一次的信息,最好用于计算手指信息

这里要使用哪个数据各位自己看着办吧,我也不是十分清晰(我这里还是使用changedTouches吧)

参数信息(changedTouches[0])

几个重要通用点:

① clientX:在显示区的坐标

② pageX:鼠标在页面上的位置

③ screenX:鼠标在显示屏上的坐标(我是双屏所以x很大)

④ target:当前元素

几个重要不同点:

① layerX:这个是相对距离,这个不同,所以不要用这个东西了

② ......

这个有必要说明下,比如我们改下代码:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
     <script id="others_zepto_10rc1" type="text/javascript" class="library" src="http://sandbox.runjs.cn/js/sandbox/other/zepto.min.js"></script>
    
</head>
<body>
<div style=" position: relative; width: 500px; height: 300px; border: 1px solid black;">
<div id="d" style=" position: absolute; top: 50px; left: 50px; width: 100px; height: 100px; border: 1px solid black;" ></div>
</div>
</body>

<script type="text/javascript">
var startTime;
var log = function (msg, e) {
    console.log(e);
    var div = $('<div></div>');
    div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg)
    $('body').append(div);

};
var touchStart = function (e) {
    startTime = new Date().getTime();
    log('touchStart', e);
};
var touchEnd = function (e) {
    log('touchEnd', e);

};
var mouseDown = function (e) {
    log('mouseDown', e);
};
var mouseClick = function (e) {
    log('mouseClick', e);
};
var mouseUp = function (e) {
    log('mouseUp', e);

};
var d = $('#d');
d.bind('mousedown', mouseDown);
d.bind('click', mouseClick);
d.bind('mouseup', mouseUp);
d.bind('touchstart', touchStart);
d.bind('touchend', touchEnd);

    </script>
</html>

测试地址

http://sandbox.runjs.cn/show/7tyo48bf

各位自己运行看看差异吧

简单扩展touch事件

touch没有click事件,于是有zepto搞了个tap事件,我们这里先来简单模拟一下,再看源码怎么干的

var mouseData = {
    sTime: 0,
    eTime: 0,
    sX: 0,
    eX: 0,
    sY: 0,
    eY: 0
};
var log = function (msg) {
    console.log(msg);
};
var touchStart = function (e) {
    var pos = e.changedTouches[0];
    mouseData.sTime = new Date().getTime();
    mouseData.sX = pos.pageX;
    mouseData.sY = pos.pageY;
};
var touchMove = function (e) {
    //        var pos = e.changedTouches[0];
    //        mouseData.eTime = new Date().getTime();
    //        mouseData.eX = pos.pageX;
    //        mouseData.eY = pos.pageY;
    e.preventDefault();
    return false;
};
var touchEnd = function (e) {
    var pos = e.changedTouches[0];
    mouseData.eTime = new Date().getTime();
    mouseData.eX = pos.pageX;
    mouseData.eY = pos.pageY;
    var data = onTouchEnd();
    log(data);
    var d = $('body');
    d.append($('<div>间隔:' + data.timeLag + ', 方向:' + data.dir + '</div>'));
};
var onTouchEnd = function () {
    //时间间隔
    var timeLag = mouseData.eTime - mouseData.sTime;
    //移动状态,默认乱移动
    var dir = 'move';
    if (mouseData.sX == mouseData.eX) {
        if (mouseData.eY - mouseData.sY > 0) dir = 'down';
        if (mouseData.eY - mouseData.sY < 0) dir = 'up';
        if (mouseData.eY - mouseData.sY == 0) dir = 'tap';
    }
    if (mouseData.sY == mouseData.eY) {
        if (mouseData.eX - mouseData.sX > 0) dir = 'right';
        if (mouseData.eX - mouseData.sX < 0) dir = 'left';
        if (mouseData.eX - mouseData.sX == 0) dir = 'tap';
    }
    return {
        timeLag: timeLag,
        dir: dir
    };
};

var touchEvents = function (el, func) {
    el = el || document;
    func = func || function () { };
    el.addEventListener('touchstart', touchStart);
    el.addEventListener('touchmove', touchMove);
    el.addEventListener('touchend', touchEnd);
};
var d = $('body');
touchEvents(d[0]);

测试地址

http://sandbox.runjs.cn/show/2n9nqssv

这里就可以看到一次touch事件是tap还是up等属性,当然很多时候我们需要设置x方向或者y方向不可拖动,这样就更好呈现

时间间隔长短可以让我们判断自己的拖动是长拖动还是短拖动,长拖动也许用户希望动画慢点,短拖动也许动画就快了

touch事件代码汇总

var log = function (msg) {
    console.log(msg);
};
var d = $('body');

var touchEvents = function (el, type, func) {
    this.long = 400; //用于设置长点击阀值
    this.el = el || document;
    this.func = func || function () { };
    this.type = type || 'tap';
    this.mouseData = {
        sTime: 0,
        eTime: 0,
        sX: 0,
        eX: 0,
        sY: 0,
        eY: 0
    };
    this.addEvent();

};
touchEvents.prototype = {
    constructor: touchEvents,
    addEvent: function () {
        var scope = this;
        this.startFn = function (e) {
            scope.touchStart.call(scope, e);
        };
        this.moveFn = function (e) {
            scope.touchMove.call(scope, e);
        };
        this.endFn = function (e) {
            scope.touchEnd.call(scope, e);
        };
        this.el.addEventListener('touchstart', this.startFn);
        //此处可以换成这样
        //            document.addEventListener('touchmove', this.touchMove);
        this.el.addEventListener('touchmove', this.moveFn);
        this.el.addEventListener('touchend', this.endFn);
    },
    removeEvent: function () {
        this.el.removeEventListener('touchstart', this.touchStart);
        this.el.removeEventListener('touchmove', this.touchMove);
        this.el.removeEventListener('touchend', this.touchEnd);
    },
    touchStart: function (e) {
        var pos = e.changedTouches[0];
        this.mouseData.sTime = new Date().getTime();
        this.mouseData.sX = pos.pageX;
        this.mouseData.sY = pos.pageY;
    },
    touchMove: function (e) {
        e.preventDefault();
        return false;
    },
    touchEnd: function (e) {
        var pos = e.changedTouches[0];
        this.mouseData.eTime = new Date().getTime();
        this.mouseData.eX = pos.pageX;
        this.mouseData.eY = pos.pageY;
        this.onTouchEnd();
    },
    onTouchEnd: function () {
        if (this.type == this._getDir()) {

        }
    },
    _getDir: function () {
        //时间间隔,间隔小于100都认为是快速,大于400的认为是慢速
        var timeLag = this.mouseData.eTime - this.mouseData.sTime;
        var dir = 'swipe';
        if (timeLag > this.long) dir = 'longSwipe';
        if (this.mouseData.sX == this.mouseData.eX && this.mouseData.sY == this.mouseData.eY) {
            dir = 'tap';
            if (timeLag > this.long) dir = 'longTap';
        } else {
            if (Math.abs(this.mouseData.eY - this.mouseData.sY) > Math.abs(this.mouseData.eX - this.mouseData.sX)) {
                dir = this._getUDDir(dir);
            } else {
                dir = 'swipe';
                dir = this._getLRDir(dir);
            }
        }
        log(dir);
        d.append($('<div>间隔:' + timeLag + ', 方向:' + dir + '</div>'));
        return dir;
    },
    //单独用于计算上下的
    _getUDDir: function (dir) {
        if (this.mouseData.eY - this.mouseData.sY > 0) dir += 'Down';
        if (this.mouseData.eY - this.mouseData.sY < 0) dir += 'Up';
        return dir;
    },
    //计算左右
    _getLRDir: function (dir) {
        if (this.mouseData.eX - this.mouseData.sX > 0) dir += 'Right';
        if (this.mouseData.eX - this.mouseData.sX < 0) dir += 'Left';
        return dir;
    }
};

new touchEvents(d[0], 'swipe', function () {
//        d.append($('<div>间隔:' + data.timeLag + ', 方向:' + data.dir + '</div>'));
});

测试地址

http://sandbox.runjs.cn/show/rpohk79w

测试时请使用chrome,并且开启touch事件

测试效果

 

完整可绑定事件代码

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <script id="others_zepto_10rc1" type="text/javascript" class="library" src="http://sandbox.runjs.cn/js/sandbox/other/zepto.min.js"></script>
</head>
<body>
    <div id="d" style="position: absolute; top: 50px; left: 50px; width: 100px; height: 100px;
        border: 1px solid black;">滑动我
    </div>
</body>
<script type="text/javascript">
var log = function (msg) {
    console.log(msg);
};
var d = $('body');

var touchEvents = function (el, type, func) {
    this.long = 400; //用于设置长点击阀值
    this.el = el || document;
    this.func = func || function () { };
    this.type = type || 'tap';
    this.mouseData = {
        sTime: 0,
        eTime: 0,
        sX: 0,
        eX: 0,
        sY: 0,
        eY: 0
    };
    this.addEvent();

};
touchEvents.prototype = {
    constructor: touchEvents,
    addEvent: function () {
        var scope = this;
        this.startFn = function (e) {
            scope.touchStart.call(scope, e);
        };
        this.moveFn = function (e) {
            scope.touchMove.call(scope, e);
        };
        this.endFn = function (e) {
            scope.touchEnd.call(scope, e);
        };
        this.el.addEventListener('touchstart', this.startFn);
        //此处可以换成这样
        //            document.addEventListener('touchmove', this.touchMove);
        this.el.addEventListener('touchmove', this.moveFn);
        this.el.addEventListener('touchend', this.endFn);
    },
    removeEvent: function () {
        this.el.removeEventListener('touchstart', this.touchStart);
        this.el.removeEventListener('touchmove', this.touchMove);
        this.el.removeEventListener('touchend', this.touchEnd);
    },
    touchStart: function (e) {
        var pos = e.changedTouches[0];
        this.mouseData.sTime = new Date().getTime();
        this.mouseData.sX = pos.pageX;
        this.mouseData.sY = pos.pageY;
    },
    touchMove: function (e) {
        e.preventDefault();
        return false;
    },
    touchEnd: function (e) {
        var pos = e.changedTouches[0];
        this.mouseData.eTime = new Date().getTime();
        this.mouseData.eX = pos.pageX;
        this.mouseData.eY = pos.pageY;
        this.onTouchEnd(e);
    },
    onTouchEnd: function (e) {
        if (this.type == this._getDir()) {
            this.func(e, this);
        }
    },
    _getDir: function () {
        //时间间隔,间隔小于100都认为是快速,大于400的认为是慢速
        var timeLag = this.mouseData.eTime - this.mouseData.sTime;
        var dir = 'swipe';
        if (timeLag > this.long) dir = 'longSwipe';
        if (this.mouseData.sX == this.mouseData.eX && this.mouseData.sY == this.mouseData.eY) {
            dir = 'tap';
            if (timeLag > this.long) dir = 'longTap';
        } else {
            if (Math.abs(this.mouseData.eY - this.mouseData.sY) > Math.abs(this.mouseData.eX - this.mouseData.sX)) {
                dir = this._getUDDir(dir);
            } else {
                dir = this._getLRDir(dir);
            }
        }
        log(dir);
        d.append($('<div>间隔:' + timeLag + ', 方向:' + dir + '</div>'));
        return dir;
    },
    //单独用于计算上下的
    _getUDDir: function (dir) {
        if (this.mouseData.eY - this.mouseData.sY > 0) dir += 'Down';
        if (this.mouseData.eY - this.mouseData.sY < 0) dir += 'Up';
        return dir;
    },
    //计算左右
    _getLRDir: function (dir) {
        if (this.mouseData.eX - this.mouseData.sX > 0) dir += 'Right';
        if (this.mouseData.eX - this.mouseData.sX < 0) dir += 'Left';
        return dir;
    }
};

new touchEvents(d[0], 'tap', function (e) {
    log(arguments);
});

</script>
</html>

这个代码基本可用了,但是使用上不是很方便,我们这里就不关注了,下面我们来看看zepto的代码和兼容问题

zepto的touch与兼容

先上zepto源码,一看就知道我写的有多不行啦!

(function ($) {
    var touch = {},
    touchTimeout, tapTimeout, swipeTimeout,
    longTapDelay = 750, longTapTimeout

    function parentIfText(node) {
        return 'tagName' in node ? node : node.parentNode
    }

    function swipeDirection(x1, x2, y1, y2) {
        var xDelta = Math.abs(x1 - x2), yDelta = Math.abs(y1 - y2)
        return xDelta >= yDelta ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
    }

    function longTap() {
        longTapTimeout = null
        if (touch.last) {
            touch.el.trigger('longTap')
            touch = {}
        }
    }

    function cancelLongTap() {
        if (longTapTimeout) clearTimeout(longTapTimeout)
        longTapTimeout = null
    }

    function cancelAll() {
        if (touchTimeout) clearTimeout(touchTimeout)
        if (tapTimeout) clearTimeout(tapTimeout)
        if (swipeTimeout) clearTimeout(swipeTimeout)
        if (longTapTimeout) clearTimeout(longTapTimeout)
        touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null
        touch = {}
    }

    $(document).ready(function () {
        var now, delta

        $(document.body)
      .bind('touchstart', function (e) {
          now = Date.now()
          delta = now - (touch.last || now)
          touch.el = $(parentIfText(e.touches[0].target))
          touchTimeout && clearTimeout(touchTimeout)
          touch.x1 = e.touches[0].pageX
          touch.y1 = e.touches[0].pageY
          if (delta > 0 && delta <= 250) touch.isDoubleTap = true
          touch.last = now
          longTapTimeout = setTimeout(longTap, longTapDelay)
      })
      .bind('touchmove', function (e) {
          cancelLongTap()
          touch.x2 = e.touches[0].pageX
          touch.y2 = e.touches[0].pageY
          if (Math.abs(touch.x1 - touch.x2) > 10)
              e.preventDefault()
      })
      .bind('touchend', function (e) {
          cancelLongTap()

          // swipe
          if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
            (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))

              swipeTimeout = setTimeout(function () {
                  touch.el.trigger('swipe')
                  touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
                  touch = {}
              }, 0)

          // normal tap
          else if ('last' in touch)

          // delay by one tick so we can cancel the 'tap' event if 'scroll' fires
          // ('tap' fires before 'scroll')
              tapTimeout = setTimeout(function () {

                  // trigger universal 'tap' with the option to cancelTouch()
                  // (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
                  var event = $.Event('tap')
                  event.cancelTouch = cancelAll
                  touch.el.trigger(event)

                  // trigger double tap immediately
                  if (touch.isDoubleTap) {
                      touch.el.trigger('doubleTap')
                      touch = {}
                  }

                  // trigger single tap after 250ms of inactivity
                  else {
                      touchTimeout = setTimeout(function () {
                          touchTimeout = null
                          touch.el.trigger('singleTap')
                          touch = {}
                      }, 250)
                  }

              }, 0)

      })
      .bind('touchcancel', cancelAll)

        $(window).bind('scroll', cancelAll)
    })

  ; ['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function (m) {
      $.fn[m] = function (callback) { return this.bind(m, callback) }
  })
})(Zepto)

touch对象与上面mouseData功效相同,记录一些属性

delta 用于记录两次点击的间隔,间隔短就是双击
swipeDirection 函数与_getDir _getUDDir _getLRDir 功能相似,只不过代码更为简练,并且真正的私有化了
63行代码开始,若是代码移动过便是划屏,否则就是点击,这点我也没考虑到
73行,否则就应该是点击,这里并且判断是否存在结束时间,代码比较健壮,做了双击或者快速点击的判断

开始兼容

zepto代码我自然没有资格去评说,现在我们来看看他的兼容问题

PS:我这里很水,不太敢动源码,就加一个tap判断,因为也只是用了这个,具体大动手脚的事情,我们后面再做

这样做事因为,我们的项目主要是把click改成了tap事件,导致页面很多功能不可用

['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function (m) {
  //兼容性方案处理,以及后期资源清理,如果为假时候,就触发点击事件
    var isTouch = 'ontouchstart' in document.documentElement;
    if(m === 'tap' && isTouch === false) {
        $.fn[m] = function (callback) { return this.bind('click', callback) }
    } else {
        $.fn[m] = function (callback) { return this.bind(m, callback) }
    }
  })

我就干了这么一点点事情......

 

感谢作者 叶小钗 给我们带来经精彩的文章!

微信号
微信公众号

tao-s.com