新年第一發–深入不淺出zepto的Tap擊穿問題

NO IMAGE

問題來源

年前去阿里面試,過程中說道了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