好久之前,addEventListener() 的參數約定是這樣的:html
addEventListener(type, listener, useCapture)
後來,最後一個參數,也就是控制監聽器是在捕獲階段執行仍是在冒泡階段執行的 useCapture 參數,變成了可選參數(傳 true 的狀況太少了),成了:react
addEventListener(type, listener[, useCapture ])
去年年末,DOM 規範作了修訂:addEventListener() 的第三個參數能夠是個對象值了,也就是說第三個參數如今能夠是兩種類型的值了:jquery
addEventListener(type, listener[, useCapture ])
addEventListener(type, listener[, options ])
這個修訂是爲了擴展新的選項,從而自定義更多的行爲,目前規範中 options 對象可用的屬性有三個:git
addEventListener(type, listener, { capture: false, passive: false, once: false })
三個屬性都是布爾類型的開關,默認值都爲 false。其中 capture 屬性等價於之前的 useCapture 參數;once 屬性就是代表該監聽器是一次性的,執行一次後就被自動 removeEventListener 掉,尚未瀏覽器實現它;passive 屬性是本文的主角,Firefox 和 Chrome 已經實現,先看個 Chrome 官方的視頻介紹(單擊播放):github
不少移動端的頁面都會監聽 touchstart 等 touch 事件,像這樣:瀏覽器
document.addEventListener("touchstart", function(e){ ... // 瀏覽器不知道這裏會不會有 e.preventDefault() })
因爲 touchstart 事件對象的 cancelable 屬性爲 true,也就是說它的默認行爲能夠被監聽器經過 preventDefault() 方法阻止,那它的默認行爲是什麼呢,一般來講就是滾動當前頁面(還多是縮放頁面),若是它的默認行爲被阻止了,頁面就必須靜止不動。但瀏覽器沒法預先知道一個監聽器會不會調用 preventDefault(),它能作的只有等監聽器執行完後再去執行默認行爲,而監聽器執行是要耗時的,有些甚至耗時很明顯,這樣就會致使頁面卡頓。視頻裏也說了,即使監聽器是個空函數,也會產生必定的卡頓,畢竟空函數的執行也會耗時。app
視頻裏還說了,有 80% 的滾動事件監聽器是不會阻止默認行爲的,也就是說大部分狀況下,瀏覽器是白等了。因此,passive 監聽器誕生了,passive 的意思是「順從的」,表示它不會對事件的默認行爲說 no,瀏覽器知道了一個監聽器是 passive 的,它就能夠在兩個線程裏同時執行監聽器中的 JavaScript 代碼和瀏覽器的默認行爲了。框架
下面是在 Chrome for Android 上滾動 cnn.com 頁面的對比視頻,右邊在註冊 touchstart 事件時添加了 {passive: true} 選項,左邊沒有,能夠看到,右邊的順暢多了。ide
假若有人不當心在 passive 的監聽器裏調用了 preventDefault(),也無妨,由於 preventDefault() 不會產生任何效果。這裏我用自定義事件演示一下這種狀況:函數
let event = new Event("foo", { // 建立一個 type 爲 foo 的事件對象,能夠被阻止默認行爲 "cancelable": true }) document.addEventListener("foo", function(event) { // 在 document 上綁定 foo 事件的監聽函數 console.log(event.defaultPrevented) // false event.preventDefault() console.log(event.defaultPrevented) // 仍是 false,preventDefault() 無效 }, { passive: true }) document.dispatchEvent(event) // 派發自定義事件
同時,瀏覽器的開發者工具也會發出警告:
Chrome 下:
Firefox 下:
除了上面在 passive 的監聽器裏調用 preventDefault() 會發出警告外,Chrome 的開發者工具還會:
1. 發現耗時超過 100 毫秒的非 passive 的監聽器,警告你加上 {passive: true}:
2. 給監聽器對象增長 passive 屬性,監聽器對象在普通頁面中是獲取不到的,能夠在 Event Listeners 面板中和經過調用 getEventListeners() Command Line API 獲取到:
Firefox 的開發者工具目前尚未這些。
之前,在第三個參數是布爾值的時候,addEventListener("foo", listener, true) 添加的監聽器,必須用 removeEventListener("foo", listener, true) 才能刪除掉。由於這個監聽器也有可能還註冊在了冒泡階段,那樣的話,同一個監聽器實際上對應着兩個監聽器對象(經過 getEventListeners() 可看到)。
那如今 addEventListener("foo", listener, {passive: true}) 添加的監聽器該如何刪除呢?答案是 removeEventListener("foo", listener) 就能夠了,passive 能夠省略,緣由是:在瀏覽器內部,用來存儲監聽器的 map 的 key 是由事件類型,監聽器函數,是否捕獲這三者組成的,passive 和 once 不在其中,理由顯而易見,一個監聽器同時是 passive 和非 passive(以及同時是 once 和非 once)是說不通的,若是你添加了二者,那麼後添加的不算,瀏覽器會認爲添加過了:
addEventListener("foo", listener, {passive: true}) addEventListener("foo", listener, {passive: false}) // 這句不算 addEventListener("bar", listener, {passive: false}) addEventListener("bar", listener, {passive: true}) // 這句不算
因此說在 removeEventListener 的時候永遠不需寫 passive 和 once,但 capture 可能要:
addEventListener("foo", listener, {capture: true}) removeEventListener("foo", listener, {capture: true}) // {capture: true} 必須加,固然 {capture: true} 換成 true 也能夠
passive 監聽器能保證的只有一點,那就是調用 preventDefault() 無效,至於瀏覽器對默認行爲卡頓的優化,那是瀏覽器的事情,是在規範要求以外的。鑑於這個新特性原本就是爲解決滾動和觸摸事件的卡頓而發明的,目前 Chrome 和 Firefox 支持優化的事件類型也僅限這類事件,好比 touchstart,touchmove,wheel 等事件,具體的事件列表我沒法提供,也許得研讀源碼才行。
但我能夠列舉幾個瀏覽器不優化的事件類型,還附帶 demo:
除了這三種事件類型外,全部 Cancelable 爲 true 的事件類型理論上都是能夠有這種優化的, 能夠看看這個 UI 事件類型列表,還有這個觸摸事件類型列表,注意 Cancelable 列。
我諮詢了 Chrome 工程師,有沒有優化滾動和觸摸事件類型以外事件類型的計劃,答覆是目前沒有:
在 passive 規範以前,Firefox 就已經有了本身對滾動觸摸行爲卡頓問題的優化,其中有個關鍵作法是,不尊重 preventDefault():若是在必定時間以內沒有調用 preventDefault(),那 Firefox 就假定你不會阻止默認滾動了,好比執行下面這句後,頁面會沒法滾動:
addEventListener("wheel", function (e) { e.preventDefault() })
但執行這句後照樣能滾動:
addEventListener("wheel", function (e) { sleep(300) e.preventDefault() // 這句在 Firefox 中無效 })
這篇博客講了 APZ 優化:Smoother scrolling in Firefox 46 with APZ
下面是從 Modernizr 裏複製的檢測腳本:
var supportsPassiveOption = false; try { var opts = Object.defineProperty({}, 'passive', { get: function() { supportsPassiveOption = true; } }); window.addEventListener('test', null, opts); } catch (e) {}
能夠這麼用:
if (supportsPassiveOption) { document.addEventListener("foo", listener, {passive: true}) // 舊瀏覽器裏第三參數會被自動轉成 true,不是咱們想要的 } else { document.addEventListener("foo", listener) }
對於在同一個 DOM 對象身上添加的同一類型事件的監聽器,只要有一個不是 passive 的,那瀏覽器就沒法優化。
https://github.com/facebook/react/issues/6436
https://github.com/angular/angular/issues/8866
https://github.com/emberjs/ember.js/issues/12783