移動端Web界面滾動性能優化: Passive event listeners

移動端Web界面滾動性能優化: Passive event listeners

今晚在閱讀VueJS2的源碼時,發現了下面的一段代碼,感覺自己瞬間知識儲備不夠用了,所以決定深入研究一下,故總結得出此文。關於VueJS的源碼解讀,之後會整理出學習筆記。這裏先簡單記錄一些碎片化的知識點。

 try {
    const opts = {}
    Object.defineProperty(opts, 'passive', ({
      get () {
        /* istanbul ignore next */
        supportsPassive = true
      }
    } : Object))
    window.addEventListener('test-passive', null, opts)
  } catch (e) {}

Passive event listeners 到底是什麼,它有什麼用

Passive event listeners是2016年Google I/O 上同 PWA 概念一起被提出,但是同PWA不同,Passive event listeners
的作用很簡單,如果用簡單一句話來解釋就是:提升頁面滑動的流暢度。

爲什麼會提出這個方案以及如何使用

想要很好的理解其用途,我們需要先來看一下傳統的事件監聽函數的寫法,以及其執行流程,這裏只做簡單的複習,更多細節可以參考《JavaScript高級程序設計》(第三版)第13章。

addEventListener 用來在頁面中監聽事件,它的參數簽名是這樣的:

target.addEventListener(type, listener[, useCapture]);

但是如果你現在去查詢 MDN 的文檔卻發現是這樣寫的:

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
target.addEventListener(type, listener[, useCapture, wantsUntrusted  ]); // Gecko/Mozilla only

最後一個參數 useCapture 在很久之前是必填的,後來的規範將 useCapture 變成了選填。useCapture 參數用來控制監聽器是在捕獲階段執行還是在冒泡階段執行,true 爲捕獲階段,false 爲冒泡階段,變成選填後默認值爲 false(冒泡階段),因爲傳 true 的情況太少了。

事件捕獲 vs 事件冒泡

先來看看經典的事件捕獲和冒泡模型:
事件捕獲和冒泡模型
事件捕獲和冒泡模型

上圖反映了JavaScript事件的傳播(冒泡)過程。如果我們爲每一層的元素都綁定事件,那麼在事件冒泡過程中,最底層的元素會最先響應事件,然後依次向父元素(上一層)冒泡。

在事件處理函數中,會傳遞 Event 對象作爲事件處理函數的參數,而這個參數最常用的 2 個方法就是

event.preventDefault();  // 阻止事件繼續傳播
event.stopPropagation(); // 取消事件的默認行爲

在移動網頁中,我們經常使用的就是 touch 系列的事件,如:

touchstart
touchmove
touchend
touchcancel

通常我們使用如下方式綁定 touch 事件:

// 由於第三個參數沒有傳值,那麼默認就是 false,事件會在冒泡階段被處理
div.addEventListener("touchstart", function(e){
    // do sth.
});

接下來我們分析一下事件的執行流程:

  1. 如果我們在事件處理函數中調用了 stopPropagation(),那麼之後的元素就無法接收這個事件,也即是剩餘的事件處理函數永遠不會得到執行。所以如果你不是非要那麼做,請千萬不要調用這個方法,否者你或者你的合作開發者會發現奇奇怪怪的Bug。

  2. 如果我們在事件處理函數中調用了 preventDefault(),那麼元素的默認行爲就會被取消。
    舉個例子來說明:一個 a 標籤綁其 click 事件的默認行爲是跳轉到 href 指定的鏈接,但是如果我們在click事件處理函數裏面調用了 preventDefault 方法後,其默認的的行爲就被取消了。

移動端列表滾動的性能點

接着我們上面的分析來進行下面的關注點。同樣的道理,如果我們在 touchstart 事件調用 preventDefault 那麼整個列表的滾動就會被取消,我們會驚奇的發現我們的頁面癱瘓了(所以在移動端,你不僅不能輕易使用 stopPropagation, 在可滾動元素的 touch 事件處理函數中,你使用 preventDefault 方法時也需要格外小心才行)。

那麼問題來了:由於瀏覽器無法預先知道一個事件處理函數中會不會調用 preventDefault(),它需要等到事件處理函數執行完後,才能去執行默認行爲,然而事件處理函數執行是要耗時的,這樣一來就會導致頁面卡頓,可以動手試試,比如在事件處理函數裏面寫一個耗時的循環試試,卡的你不要不要的(如果設置了css樣式:-webkit-overflow-scrolling : touch; 在版本稍高一點的Chrome可能會爲了優化性能而忽略 preventDefault 的調用,這也是一個可能的優化點哈)。

來看一段 Chrome 官方發佈的數據統計:

For instance, in Chrome for Android 80% of the touch events that block scrolling never actually prevent it. 10% of these events add more than 100ms of delay to the start of scrolling, and a catastrophic delay of at least 500ms occurs in 1% of scrolls.
在 Android 版 Chrome 瀏覽器的 touch 事件監聽器的頁面中,80% 的頁面都不會調用 preventDefault 函數來阻止事件的默認行爲。在滑動流暢度上,有 10% 的頁面增加至少 100ms 的延遲,1% 的頁面甚至增加 500ms 以上的延遲。
也就是說,當瀏覽器等待執行事件的默認行爲時,大部分情況是白等了。如果 Web 開發者能夠提前告訴瀏覽器:“我不調用 preventDefault 函數來阻止事件事件行爲”,那麼瀏覽器就能快速生成事件,從而提升頁面性能。

Chrome官方有個視頻測試:https://www.youtube.com/watch?v=NPM6172J22g

介紹了那麼多,終於可以告訴你 passive 就是爲此而生的。在 WICG 的 demo 中提到,即使滾動事件裏面寫一個死循環,瀏覽器也能夠正常處理頁面的滑動

在最新的 DOM 規範中,事件綁定函數的第三個參數變成了對象:

target.addEventListener(type, listener[, options]);

我們可以通過傳遞 passive 爲 true 來明確告訴瀏覽器,事件處理程序不會調用 preventDefault 來阻止默認滑動行爲。親測發現在 Chrome 瀏覽器中,如果發現耗時超過 100 毫秒的非 passive 的監聽器,會在 DevTools 裏面警告你加上 {passive: true}。Chrome 51 和 Firefox 49 已經支持 passive 屬性。如果瀏覽器不支持,已經有牛人做了非常巧妙地 polyfill, 也就是文章開始提到的 VueJS 源碼裏面的那一段代碼:

// Test via a getter in the options object to see 
// if the passive property is accessed
var supportsPassive = false;
try {
  var opts = Object.defineProperty({}, 'passive', {
    get: function() {
      supportsPassive = true;
    }
  });
  window.addEventListener("test", null, opts);
} catch (e) {}
// Use our detect's results. 
// passive applied if supported, capture will be false either way.
elem.addEventListener(
  'touchstart',
  fn,
  supportsPassive ? { passive: true } : false
);

如果想要了解給更多關於事件監聽,處理的知識,牆裂建議仔細閱讀 MDN-EventTarget.addEventListener API 文檔

發佈了106 篇原創文章 · 獲贊 79 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章