移动端javascript之事件

@kuitos 2015-08-23 09:39:40发表于 kuitos/kuitos.github.io javascript

移动端javascript之事件

原文写于 2014-12-23

Javascript的事件体系想必大家都已经耳熟能详了,它是支撑起前端交互的支柱,当然我们这里不讲DOM2 抑或DOM3 里的标准事件,这里来说说基于移动端的、单点触碰事件(多点及手势事件后续有空咱再聊)。
移动端事件的标准最初是由苹果safari团队制定,用于触碰设备的交互,包含以下几种事件:

  1. touchstart 当手指触摸到屏幕时触发。屏幕任何区域,记住,是任何区域!
  2. touchmove 当手指在屏幕上滑动时连续的触发。中途可以通过调用event.preventDefault()阻止滚动
  3. touchend 当手指从屏幕上移开时触发
  4. touchcancel 当系统停止跟踪触摸时触发

那么问题来了,我们要如何监听click事件呢?
传统的click事件不受终端限制,该怎么用还是怎么用。
假设有这么一段代码:

<div>
    <div onclick=""></div>
    <div id="test" onclick=""></div>
    <div onclick=""></div>
</div>

那么问题又来了,我们点击了id=test的div,那么事件触发的顺序是?

  1. touchstart
  2. touchend
  3. mouseover
  4. mousemove
  5. mousedown
  6. mouseup
  7. click

没错,click事件最后触发的。
如果你是个对性能敏感的程序员,你或许会问,这些事件触发的时间是?(一次实验结果,统计癖们请自行测试然后取平均值)

  1. touchstart 0ms
  2. touchend 53ms
  3. mouseover 353ms
  4. mousemove 354ms
  5. mousedown 355ms
  6. mouseup 356ms
  7. click 356ms

是的,整整 300+ ms 的delay啊尼玛!或许你会说,不要绑定click,绑定touchstart不就好了吗?nice,好主意!

可是你一旦这么做会发现你的也没在移动终端的误操作率会由以前5%激升至50%,比如你想做滚动页面操作的时候触发了某个click事件,你在想双指手势放大时又触发了某个click事件,整个世界都不好了。。。

好在有问题出现总归有相应解决方案,我们毕竟不是第一个吃螃蟹的。
如果你有兴趣研究下angular官方的ngTouch模块的代码的,会发现google团队是这么处理的(抽离细节):

当点击一个元素时

  1. 触发rootElement(就是你ngApp所在的元素)下的touchstart事件,即一个全局的touchstart事件。该事件回调会设置一个可达区域(allowable region)
  2. 触发element的touchstart事件,记录点击坐标,同时将动作设置为tap点击
  3. 触发element的touchend事件,判断是否是tap点击,判断依据的是是否有一个allowable region,以及touchstart是否触发等条件。然后手动trigger element click,同时阻止事件冒泡(这里标红是因为这里是个坑。。),即event.stopPropagation。最后移除allowable region。

这是一种典型的绕道式解决方案。不是从技术层面解决问题(是的你没办法去直接修改浏览器底层的C++代码),他做的只是换了一种思路,既然不能改变浏览器会将touchstart最后冒泡成click事件中间需要至少300ms的这个事实,那么我们干脆抛弃掉浏览器帮我们识别出来的click,直接获取touchstart事件,然后通过一套完整的逻辑判断当前touchstart是否可被判定为click,然后手动触发click回调。是的,既然现有的东西支撑不了我的需求,那么我就排列组合成我想要的东西。
但是问题又来了,angular-touch有一个很明显的不足,就是他只能通过ng-click指令来实现将相应的click事件变成看似无延时的。他无法解决通过js手动绑定的click事件的触发时机。上代码吧(是的码字解释有时候真的是太费劲。。Talk is cheap, show me the code — Linus Torvalds)

ng-click指令绑定的事件此时无延时

<div ng-click="showMe()">lalalalala</div>

通过js手动绑定的click还是会延时。。

element.bind("click", showMe);

好在开源世界是伟大的,早早有人帮我们解决了这个问题,看这里Fastclick(尼玛GFW你现在想屏蔽github是想让国内的程序员都失业么!!)
用法真心简单,拿angular举例,你只需要在你的run block里加入这样一段:

.run(["$rootScope", "app", function ($rootScope, app) {

    window.FastClick.attach(window.document.body, {
        tapDelay: 0
    });

}]);

FastClick的解决方案跟angular-touch的方案思路大致一样(几乎现有的所有解决方案都差不多是这种),它会在你设置的监听区,比如上面的document.body,添加一个touchstart事件,你点击的具体元素的touchstart会首先冒泡至body,然后被其捕获,然后通过触发点坐标计算其是否为一个点击事件,如果符合条件则会手动构建一个MouseEvents并将其声明为click,再然后dispatchEvent(event)。这样就会直接触发元素绑定的click事件,从而避免浏览器依次触发产生的延时。

最后,关于为啥会有这300ms的delay,浏览器厂商(其实就是safari)是这样解释的,我们要通过这300ms来判断你是单击还是双击,从而决定我们到底要触发什么行为。看看官方解释:

...mobile browsers will wait approximately 300ms from the time that you tap the button to fire the click event. The reason for this is that the browser is waiting to see if you are actually performing a double tap.

其实现在很多android端的浏览器能通过判断你是否在你的html声明禁用页面缩放来决定是否需要300ms delay的,因为双击主要是用来缩放的。代码这样写:

<meta name="viewport" content="user-scalable=no">

不过任性的ios目前为止还是不支持这个检测,但好在fastclick也足够轻量级和易用。so,就是这么简单。