angular-touch模块与hammerjs风波

前个礼拜刚开发完一个移动端项目,早前引入了angular-touch#1.2.28模块,发现点击似乎有延迟,当时没太在意,后面遇到了新的手势需求,google到hammerjs这个移动端手势神器,引入封装之(通过angular-gestures)。后来由于一系列原因又把hammer干掉了,全部采用了angular-touch模块中的ng-click和ng-swipe-left/ng-swipe-right,尚能满足需求。

与后端联调时,后端跟我反馈点击延迟严重,我便尝试升级angular#1.4.2及其所有插件(其实主要是angular-touch,但只升一个,可能会产生其他问题),让我惊喜的是,ng-click的响应果然快了许多。当时以为忙,没细细研究原因,加入到todo list中。此次升级还遇到了onpopstate与$location服务的问题,周末研究了一下午才找到原因,并hack之,这个问题有机会另开一篇讲。

后来参与到一个混合iOS App开发中,需要实现JS调用ios的native接口,结果就暴露出ng-click的一系列问题,最终替换成hammerjs的事件绑定,一切问题迎刃而解。

到今天终于有时间研究一下ng-click的源码了。对比了一下1.2.28和1.4.2的源码,并作了试验。关键区别是:

左边是1.2.28版本的源码,是touchend事件回调的代码,变量e是拿不到事件对象的,紧接着下面的x和y也拿不到clientX和clientY,下面的那个if也始终为false,导致没有执行下面的

1
element.triggerHandler('click', [event]);

最终执行的是浏览器默认的有300ms延时的回调。其实这算是一个bug了,但是在1.4.2中却在特殊情况下也产生了一个问题。先来简单说明一下我了解到的它click处理机制:

先说这段代码吧:

1
2
3
4
5
element.on('click', function(event, touchend) {
scope.$apply(function() {
clickHandler(scope, {$event: (touchend || event)});
});
});

虽然几乎是放在最后的,但这里是触发我们controller里click回调的最明显最“直接”上层调用栈(暂时忽略scope.$apply的底层代码)。

然后再说之前的逻辑,它为使用了ng-click指令的元素,绑定了touchstart,touchend(ouchcanel,mousedown,mousemove,mouseup比较次要,忽略)。touchstart记录了起始点击坐标,起始点击时间,touchedn记录了终点坐标,点击的持续时间,如果两次坐标与点击持续时间在指定范围内则手动触发click回调(jquery的API),的确解决了300ms延时的问题。但是300ms之后,“正常触发click事件”还是会发生,于是在touchend里还会做一件事,就是根据具体情况判断是否阻止“正常触发click事件”。preventGhostClick是被调用的函数,作者在这里似乎花了不少笔墨,我至今没理解这样做的理由,也许和为了兼容PC浏览器有关吧:

它为整个angular根元素(即ng-app对应的高层节点)绑定了touchstart和click事件,而且开启了捕获模式!

1
2
3
4
5
6
7
8
9
10
11
function preventGhostClick(x, y) {
if (!touchCoordinates) {
$rootElement[0].addEventListener('click', onClick, true);//开启了捕获模式
$rootElement[0].addEventListener('touchstart', onTouchStart, true);//开启了捕获模式
touchCoordinates = [];
}
lastPreventedTime = Date.now();
checkAllowableRegions(touchCoordinates, x, y);
}

问题就发生在这里的click回调,头一句就使用了时间判定

1
2
3
4
5
6
7
8
function onClick(event) {
if (Date.now() - lastPreventedTime > PREVENT_DURATION) {
return; // Too old.
}
//...
event.stopPropagation();
event.preventDefault();
}

按作者的思路,正常tap点击情况下,第一个if都会是false,并且最终会执行底部的两句阻止冒泡和阻止默认事件,来达到阻止300ms延迟执行的回调。我们自己的回调肯定是比这个onClick先执行的,如果我们在自己的回调里调用了浏览器全局函数比如window.alert,window.confirm等,这些native函数是会阻塞javascript执行的。上述代码中PREVENT_DURATION的值是2500ms,假设我们自己的回调中一个window.alert弹出后,我们3秒钟后再点击“确定”键,我们的click回调将再执行一次!!!我又做了一个试验,我故意在自己的回调中,设置了一个3秒的阻塞,结果是一样执行了两次:

当然,一般情况下我们的回调不会执行这么长时间,可是会不会用window.alert,window.confirm这些原生API就不好说了,万一用了,用户会不会在2.5秒内把它关掉又不好说了。

 

总结

如果你的移动端angular项目不使用window.alert,window.confirm等这些阻塞型API,那么使用angular-touch一般不会有问题,不过angular-touch提供的移动端事件也仅限于click(tap),swipe-left,swipe-right。我个人建议还是使用hammerjs(angular-gestures)来代替angular-touch吧,手势更加丰富。

BTW,window.alert、window.confirm等浏览器原生API会阻塞javascript(setTimeout、setInterval等将中断),这一点相信有一定基础的前端都会知道。但是在混合APP中,javascript调用native的UIAlertView(长的和浏览器的alert很像奥),会不会阻塞javascript呢?答案是不会(至少在ios平台是这样的)!这很出乎我的意料,本人亲测!由于这个测试相对复杂,就暂不上图了。