你真的會用addEventListener嗎?
沒錯,的確很標題黨,但是我最近發現我的一個朋友(誰說是我的!)真的不太會用addEventListener
。
起因
我最近參與了一個開源專案,vue-carousel,Vue生態圈裡的一個輪播元件,由加拿大的一個電商公司SSENSE開源的。然後剛好有人提了一個issue
Unable to preventDefault inside passive event listener due to target being treated as passive
也許有人在開發移動端應用的時候遇到過這個問題,我來為大家復現一下這個場景:
當我們給document
新增了touch
事件的監聽器的時候,如果同時在handler內部呼叫了event.preventDefault()
,這時候瀏覽器(Chrome 56+)就會報一個warning:Unable to preventDefault inside passive event listener due to target being treated as passive
這句警告翻譯過來就是:不能給passive(被動的)事件監聽器preventDefault
,因為它被認為是passive。
什麼意思呢?不著急,首先,我們來看一下什麼叫passive event listener
。但是在這之前,我們還是得複習一下addEventListener
的第三個引數。
不為所知的第三個引數
當我們使用addEventListener
的時候,我們一般的寫法是以下:
target.addEventListener(event, handler)
複製程式碼
相信還有人使用更完整的寫法:
target.addEventListener(event, handler, false)
複製程式碼
是的,相信很多人也已經知道了,addEventListener
方法是有第三個引數的,我們時常傳入一個false
來作為這第三個引數。但是我們不傳其實也一樣,因為這第三個引數預設就是false
,是不是覺得自己不明所以地寫了很多冤枉程式碼還自以為很嚴謹?
哈哈,所以我們得來看看,這第三個引數到底是做什麼的。
常規操作下,這第三個引數是一個布林值,叫useCapture
,也就是指在DOM樹中,註冊了該listener的元素,是否會先於它下方的任何事件目標,接收到該事件。
我們知道,DOM事件流(event flow)存在三個階段:事件捕獲階段、處於目標階段、事件冒泡階段。如果useCapture
設定為false
,當前eventTarget就不會在捕獲階段接收該事件。瀏覽器預設我們不會在捕獲階段觸發繫結事件的handler。
但是我相信還是有很多人沒有認真看過addEventListener
的文件,第三個引數其實並不一定是一個布林值。他也可以是一個物件,一組配置。
{
capture: Boolean, // 表示`listener`會在該型別的事件捕獲階段傳播到該`EventTarget`時觸發
once: Boolean, // 表示`listener`在新增之後最多隻呼叫一次。如果是`true`,`listener`會在其被呼叫之後自動移除
passive: Boolean, // 表示`listener`永遠不會呼叫`preventDefault()`。如果`listener`仍然呼叫了這個函式,客戶端將會忽略它並丟擲一個控制檯警告
}
複製程式碼
除以上之外,還有一個mozSystemGroup
,我們暫時不討論。
所以其實我們還可以在useCapture
的基礎上另外配置兩個配置項。
once
表示listener
在新增之後最多隻呼叫一次。如果是true
,listener
會在其被呼叫之後自動移除,這跟我們在jQuery時代的once
方法比較像。
passive
表示listener
永遠不會呼叫preventDefault()
。如果listener
仍然呼叫了這個函式,客戶端將會忽略它並丟擲一個控制檯警告。具體的我們接著討論。
passive event listener
在具體討論passive event listener
之前,我們先普及一個知識點。大家可以自己去看看英文文件,不過需要科學上網。
簡而言之就是當我們在滾動頁面的時候(通常是我們監聽touch事件的時候),頁面其實會有一個短暫的停頓(大概200ms),瀏覽器不知道我們是否要preventDefault
,所以它需要一個延遲來檢測。這就導致了我們的滑動顯得比較卡頓。
從Chrome 51開始,passive event listener
被引進了Chrome,我們可以通過對addEventListener
的第三個引數設定{ passive: true }
來避免瀏覽器檢測這個我們是否有在touch事件的handler裡呼叫preventDefault
。在這個時候,如果我們依然呼叫了preventDefault
,就會在控制檯列印一個警告。告訴我們這個preventDefault
會被忽略。
當我們給addEventListener
的第三個引數設定了{ passive: true }
,這個事件監聽器就被稱為passive event listener。
從Chrome 56開始,如果我們給document
繫結touchmove
或者touchstart
事件的監聽器,這個passive
是會被預設設定為true
以提高效能,具體chromestatue 文件。但是我們大多數人並不知道這點,並且依舊呼叫了preventDefault
。這並不會導致什麼頁面崩潰級的錯誤,但是這可能導致我們忽略了一個頁面效能優化的點,特別是在移動端這種更加重視效能優化的場景下。
相容性
第三個引數是在近段時間才被調整為一組配置項,如果我們需要相容舊版瀏覽器,我們需要寫一些檢測程式碼。
var passiveSupported = false;
try {
var options = Object.defineProperty({}, "passive", {
get: function() {
passiveSupported = true;
}
});
window.addEventListener("test", null, options);
} catch(err) {}
複製程式碼
這段程式碼為passive
屬性建立了一個帶有getter函式的options
物件;getter設定了一個標識,passiveSupported
,被呼叫後就會把其設為true
。那意味著如果瀏覽器檢查options
物件上的passive
值時,passiveSupported
將會被設定為true
;否則它將保持false
。然後我們呼叫addEventListener()
去設定一個指定這些選項的空事件處理器,這樣如果瀏覽器將第三個引數認定為物件的話,這些選項值就會被檢查。
你可以利用這個方法檢查options之中任一個值。只需使用與上面類似的程式碼,為選項設定一個getter。然後,當你想實際建立一個是否支援options的事件偵聽器時,你可以這樣做:
someElement.addEventListener("mouseup", handleMouseUp, passiveSupported ? { passive: true } : false);
複製程式碼
我們在someElement
這裡新增了一個mouseup。對於第三個引數,如果passiveSupported
是true
,我們傳遞了一個passive
值為true
的options
物件;如果相反的話,我們知道要傳遞一個布林值,於是就傳遞false
作為useCapture
的引數。
PS: 在vue-carousel的那個bug的最開始的PR裡,我將addEventListener
的passive
設定為了false
,但是這其實不是最優解,最優解應該是去掉監聽器裡的preventDefault
,我已經提了另一個PR來解決這個問題。
參考: