新年第一发--深入不浅出zepto的Tap击穿问题
dasea_sky
8年前
<p><img src="https://simg.open-open.com/show/a5ecad9d9d4b9ee9c22677e45449d710.jpg"></p> <h2>问题来源</h2> <p>年前去阿里面试,过程中说道了fastclick解决iPhone机器上300ms点击延迟的问题,然后就被问到了zepto的“点击穿透”的现象以及产生这个具体原因,当时回答的不是很好,主要是没有特别深入的去研究这个原因,只是知道有这个现象和问题,大概怎么解决,面试完了之后有一天突然想起来了,就决定仔细的研究下。</p> <p>其实有好多文章都写了,内容有很多我就不重复,总结以下几点:</p> <ol> <li> <p>300ms延迟是由于浏览器要判断是单机还是双击造成的延迟处理点击事件</p> </li> <li> <p>fastclick解决方式用touchstart结合touchmove以及touchend替代click事件</p> </li> <li> <p>zepto的tap会“击穿”页面是由于既响应了自身的tap(也就是touch事件),又没有拦截掉原来的click事件,导致重复执行了2次事件,在有遮罩弹层的时候就会出现“击穿”效果。如果不太明白的话看这篇文章 <a href="/misc/goto?guid=4959737672516975641" rel="nofollow,noindex"> zepto的击穿 </a></p> </li> </ol> <h2>年前探究</h2> <p>当时研究到这里时候我有一个大大的疑问就是为什么click延迟执行之后,遮罩层下面的页面的click事件会被触发,我明明点击的遮罩层的A按钮,为何下面页面的B按钮的事件会执行。按照我最初的想法,应该是继续执行A按钮的事件啊!!!此时我内心是这样的</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/3d4ede7d3f55fe16b1107035dd6b8854.jpg"></p> <p>于是我开始探究这个问题,我搜了下大概的资料,基本都没有讲这个具体原因的,也许是我打开方式不对,反正没有找到,无奈之下,我只能翻看fastclick的源码来看它为何没有出现这个问题,然后看到了sendClick的代码,心里猛然有了一个猜想。</p> <pre> <code class="language-javascript">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); };</code></pre> <p>注意这里的initMouseEvent,当时就在想肯定和mouseEvent执行的原理有关了,到这个阶段算是有了眉目。</p> <h2>接着搞</h2> <p>紧接着,开始过年,过年期间享受了生活,并没有碰代码和文档(好堕落的感觉......),加上我跳槽的空档和折腾,年后稍稍稳定下来了,最近又想起了年前这探究一半的猜想,开始继续搞了起来,顺便收收心,好进入状态。</p> <p>先说猜想--click事件最开始其实在浏览器当中被捕捉的时候,只有mouseEvent的相关属性,也就是我们平常在console.log(event)的一部分,之后,浏览器才会结合html,js产生我们常说的click时间,接着触发我们使用js绑定的函数。</p> <p><img src="https://simg.open-open.com/show/077169e5234de6d935230f78ba8e4bfe.png"></p> <p><strong><u>一般情况的event的各种属性</u> </strong></p> <p>基于这个猜想,我开始翻阅 <a href="/misc/goto?guid=4959737672617758568" rel="nofollow,noindex"> mozilla </a> 和 <a href="/misc/goto?guid=4959737672713157779" rel="nofollow,noindex"> W3C </a> 的文档来了解mouseEvent。</p> <p>翻看文档之后发现mouseEvent果然只有 screenX,screenY,clientX,clientY,ctrlKey,altKey,shiftKey,metaKey,button,buttons,EventTarget?relatedTarget。</p> <p>其中button和buttons指的是鼠标的按钮类型,就是左键,右键,滚轮这些。用数字代替,0表示左键,1是滚轮,2是右键,其他更多功能键,都是大于2的。</p> <p>从上面我们能看出来,其实对于mouseEvent而言,它只知道我们在屏幕的哪个位置,做了什么动作(鼠标操作),并不知道是在哪个element上面。这也就是fastclick还原用户点击事件最后做的事情。</p> <pre> <code class="language-javascript">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</code></pre> <p>初始化一个鼠标事件,然后dispatch这个鼠标事件。浏览器自动响应后续处理。</p> <p>接着来看click的定义,如下图所示:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/64aed5606eff08871f1367037f2e8e20.png"></p> <p><strong><u>click的属性</u> </strong></p> <p>click会多了Event.target,而且必须是一个 <a href="/misc/goto?guid=4959737672817883961" rel="nofollow,noindex"> topmost event target </a> ,在mozilla定义有些不太相同,多了currentTarget和type等。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/091337766eaa5548ce597a2babad3c98.png"></p> <p><strong><u>mozilla的click</u> </strong></p> <p>先来看EventTarget的定义:EventTarget is an interface implemented by objects that can receive events and may have listeners for them.</p> <p>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.</p> <p>从定义就能看出来了,如果是click事件必须要有一个target来承载这次鼠标事件。一般来说target要么是element要么是document,如果都没有那么就是window对象了。到这里大家应该就比较明白,这里就是浏览器的 事件机制 了。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/db2d68efdce226a4e89f8fd6541ca2f2.png"></p> <p><strong><u>event-flow</u> </strong></p> <p>这里就应该是initMouseEvent之后,浏览器干的事情,来寻找是否有target来响应此次事件,如果前面一直没有target来响应,最后就会到window上,一般来说我们不会在window上做事件处理,就会没有任何响应,事件结束了。如果碰巧的事,此时有target(一般来说就是element了)来响应,那么就会执行绑定的函数了。</p> <p>总结下整个流程:用户点击屏幕,300ms之内,浏览器拦截下这个行为,没有去真正触发相关element上绑定的click事件执行函数,而是记录操作相关数据,等待接下来的操作,由于我们使用zepto库绑定了tap事件,事件中有监听touchend触发了,立刻执行相关操作,隐藏了弹层。300ms到了,浏览器认为这次动作是click而不是dbclick,然后init一次mouseEvent在相同的屏幕位置,接着开始事件机制,发现相同位置有一个element绑定了click处理函数,执行这个函数,Over!!!穿透就是这样产生的。PS:浏览器行为部分是猜测,未验证。</p> <p>至于解决方案:网上有很多,目前最好的是fastclick,不过fastclick也会有其他问题,例如在滑动中点击之类的。另外就是用zepto但是要preventDefault。</p> <p>Android自己chrome已经解决了,可以用其他方式, 目前Safari也支持了,不过是在高版本上,相关讨论可以看fastclick的 issue</p> <p> </p> <p>来自:https://zhuanlan.zhihu.com/p/25280160</p> <p> </p>