新年第一发 -- 深入不浅出 zepto 的 Tap 击穿问题

931 阅读5分钟

问题来源


年前去阿里面试,过程中说道了fastclick解决iPhone机器上300ms点击延迟的问题,然后就被问到了zepto的“点击穿透”的现象以及产生这个具体原因,当时回答的不是很好,主要是没有特别深入的去研究这个原因,只是知道有这个现象和问题,大概怎么解决,面试完了之后有一天突然想起来了,就决定仔细的研究下。

其实有好多文章都写了,内容有很多我就不重复,总结以下几点:

  1. 300ms延迟是由于浏览器要判断是单机还是双击造成的延迟处理点击事件

  2. fastclick解决方式用touchstart结合touchmove以及touchend替代click事件

  3. zepto的tap会“击穿”页面是由于既响应了自身的tap(也就是touch事件),又没有拦截掉原来的click事件,导致重复执行了2次事件,在有遮罩弹层的时候就会出现“击穿”效果。如果不太明白的话看这篇文章zepto的击穿

年前探究

当时研究到这里时候我有一个大大的疑问就是为什么click延迟执行之后,遮罩层下面的页面的click事件会被触发,我明明点击的遮罩层的A按钮,为何下面页面的B按钮的事件会执行。按照我最初的想法,应该是继续执行A按钮的事件啊!!!此时我内心是这样的


于是我开始探究这个问题,我搜了下大概的资料,基本都没有讲这个具体原因的,也许是我打开方式不对,反正没有找到,无奈之下,我只能翻看fastclick的源码来看它为何没有出现这个问题,然后看到了sendClick的代码,心里猛然有了一个猜想。

FastClick.prototype.sendClick = function(targetElement, event) {
    var clickEvent, touch;
    // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
    if (document.activeElement && document.activeElement !== targetElement) {
        document.activeElement.blur();
    }
    touch = event.changedTouches[0];
    // Synthesise a click event, with an extra attribute so it can be tracked
    clickEvent = document.createEvent('MouseEvents');
    clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
    clickEvent.forwardedTouchEvent = true;
    targetElement.dispatchEvent(clickEvent);
    };

注意这里的initMouseEvent,当时就在想肯定和mouseEvent执行的原理有关了,到这个阶段算是有了眉目。

接着搞

紧接着,开始过年,过年期间享受了生活,并没有碰代码和文档(好堕落的感觉......),加上我跳槽的空档和折腾,年后稍稍稳定下来了,最近又想起了年前这探究一半的猜想,开始继续搞了起来,顺便收收心,好进入状态。

先说猜想--click事件最开始其实在浏览器当中被捕捉的时候,只有mouseEvent的相关属性,也就是我们平常在console.log(event)的一部分,之后,浏览器才会结合html,js产生我们常说的click时间,接着触发我们使用js绑定的函数。



一般情况的event的各种属性

基于这个猜想,我开始翻阅mozillaW3C的文档来了解mouseEvent。

翻看文档之后发现mouseEvent果然只有 screenX,screenY,clientX,clientY,ctrlKey,altKey,shiftKey,metaKey,button,buttons,EventTarget?relatedTarget。

其中button和buttons指的是鼠标的按钮类型,就是左键,右键,滚轮这些。用数字代替,0表示左键,1是滚轮,2是右键,其他更多功能键,都是大于2的。

从上面我们能看出来,其实对于mouseEvent而言,它只知道我们在屏幕的哪个位置,做了什么动作(鼠标操作),并不知道是在哪个element上面。这也就是fastclick还原用户点击事件最后做的事情。

clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
// detremineEvenType是fastclick封装返回mouseEvent的type类型,就是click还是mouseDown

初始化一个鼠标事件,然后dispatch这个鼠标事件。浏览器自动响应后续处理。

接着来看click的定义,如下图所示:



click的属性


click会多了Event.target,而且必须是一个 topmost event target,在mozilla定义有些不太相同,多了currentTarget和type等。



mozilla的click

先来看EventTarget的定义:EventTarget is an interface implemented by objects that can receive events and may have listeners for them.

Element, document, and window are the most common event targets, but other objects can be event targets too, for example XMLHttpRequest, AudioNode,AudioContext, and others.

从定义就能看出来了,如果是click事件必须要有一个target来承载这次鼠标事件。一般来说target要么是element要么是document,如果都没有那么就是window对象了。到这里大家应该就比较明白,这里就是浏览器的事件机制了。




event-flow

这里就应该是initMouseEvent之后,浏览器干的事情,来寻找是否有target来响应此次事件,如果前面一直没有target来响应,最后就会到window上,一般来说我们不会在window上做事件处理,就会没有任何响应,事件结束了。如果碰巧的事,此时有target(一般来说就是element了)来响应,那么就会执行绑定的函数了。

总结下整个流程:用户点击屏幕,300ms之内,浏览器拦截下这个行为,没有去真正触发相关element上绑定的click事件执行函数,而是记录操作相关数据,等待接下来的操作,由于我们使用zepto库绑定了tap事件,事件中有监听touchend触发了,立刻执行相关操作,隐藏了弹层。300ms到了,浏览器认为这次动作是click而不是dbclick,然后init一次mouseEvent在相同的屏幕位置,接着开始事件机制,发现相同位置有一个element绑定了click处理函数,执行这个函数,Over!!!穿透就是这样产生的。PS:浏览器行为部分是猜测,未验证。

至于解决方案:网上有很多,目前最好的是fastclick,不过fastclick也会有其他问题,例如在滑动中点击之类的。另外就是用zepto但是要preventDefault。

Android自己chrome已经解决了,可以用其他方式,官方文档,目前Safari也支持了,不过是在高版本上,相关讨论可以看fastclick的 issue