[譯] 現代瀏覽器內部揭祕(第四部分)

阿湯哥也攪基發表於2018-10-19

使用者輸入行為與合成器

內部揭祕系列部落格對現代瀏覽器如何處理程式碼、顯示頁面展開探討。該系列部落格共四篇,這是最後一篇。在上篇部落格裡,我們瞭解了 渲染程式與合成器。這裡我們將一窺當使用者輸入行為發生時,合成器如何繼續保障互動流暢。

瀏覽器視角下的輸入事件

聽到“輸入事件”這個字眼,你腦海裡閃現的恐怕只是輸入文字或點選滑鼠。但在瀏覽器眼中,輸入意味著一切使用者行為。不單滾動滑鼠滑輪是輸入事件,觸控螢幕、滑動滑鼠同樣也是使用者輸入事件。

諸如觸控螢幕之類使用者手勢產生時,瀏覽器程式會率先將其捕獲。然而瀏覽器程式所掌握的資訊僅限於行為發生的區域,因為標籤頁裡的內容都由渲染程式負責處理,所以瀏覽器程式會將事件型別(如 touchstart)及其座標傳送給渲染程式。渲染程式會尋至事件目標,執行其事件監聽器,妥善地處理事件。

input event

圖 1:輸入事件由瀏覽器程式發往渲染程式

合成器接收輸入事件

composit.gif

圖 2:懸於頁面圖層的檢視視窗

在上篇文章裡,我們探討了合成器如何通過合成柵格化圖層,實現流暢的頁面滾動。如果頁面上沒有新增任何事件監聽,合成器執行緒會建立獨立於主執行緒的新合成幀。但要是頁面上新增了事件監聽呢?合成器執行緒又是如何得知事件是否需要處理的?

理解非立即可滾動區

因為執行 JavaScript 指令碼是主執行緒的工作,所以頁面合成後,合成程式會將頁面裡新增了事件監聽的區域標記為“非立即可滾動區”。有了這個資訊,如果輸入事件發生在這一區域,合成程式可以確定應將其發往主執行緒處理。如輸入事件發生在這一區域之外,合成程式則確定無需等待主執行緒,可繼續合成新幀。

limited non fast scrollable region

圖 3:非立即可滾動區輸入描述示意圖

設定事件處理器時須注意

web 開發中常用的事件處理模式是事件代理。因為事件會冒泡,所以你可以在最頂層的元素中新增一個事件處理器,用來代理事件目標產生的任務。下面這樣的程式碼,你可能見過,或許也寫過。

document.body.addEventListener('touchstart', 
event => {
    if (event.target === area) {
        event.preventDefault();
    }
});
複製程式碼

這樣只需新增一個事件監聽器,即可監聽所有元素,的確十分省事。然而,如果站在瀏覽器的角度去考量,這等於把整個頁面都標記成了“非立即可滾動區”,意味著即便你設計的應用本不必理會頁面上一些區域的輸入行為,合成執行緒也必須在每次輸入事件產生後與主執行緒通訊並等待返回。如此則得不償失,使原本能保障頁面滾動流暢的合成器沒了用武之地。

full page non fast scrollable region

圖 4:非立即可滾動區覆蓋整個頁面下的輸入描述示意圖

你可以給事件監聽新增一個 passive:true 選項 ,將這種負面效果最小化。這會提示瀏覽器你想繼續在主執行緒中監聽事件,但合成器不必停滯等候,可接著建立新的合成幀。

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});
複製程式碼

檢查事件是否可撤銷

page scroll

圖 5:部分割槽域僅可水平方向滾動的網頁

設想一下這種情形:頁面上有一個盒子,你要將其滾動方向限制為水平滾動。

為目標事件設定 passive:true 選項可讓頁面滾動平滑,但在你使用 preventDefault 以限制滾動方向時,垂直方向滾動可能已經觸發。使用 event.cancelable 可以檢查並阻止這種情況發生。

document.body.addEventListener('pointermove', event => {
    if (event.cancelable) {
        event.preventDefault(); // 阻止預設的滾動行為
        /*
        *  這裡設定程式執行任務
        */
    } 
}, {passive:: true});
複製程式碼

或者,你也可以應用 touch-action 這類 CSS 規則,完全地將事件處理器遮蔽掉。

#area { 
  touch-action: pan-x; 
}
複製程式碼

定位事件目標

hit test

圖 6:主執行緒檢查繪製記錄查詢座標 x、y 處繪製內容

合成器將輸入事件傳送至主執行緒後,首先執行的是命中檢測。命中檢測會使用渲染程式中產生的繪製記錄資料,找出事件發生座標下的內容。

降低往主執行緒傳送事件的頻率

之前的文章裡,我們探討了常見螢幕如何以每秒 60 幀的頻率重新整理,以及我們要怎樣與其重新整理頻率保持步調一致,以營造出流暢的動畫效果。而對於使用者的輸入行為,常見觸控式螢幕裝置的事件傳輸頻率為每秒 60~120 次,常見滑鼠裝置的事件傳輸頻率為每秒 100 次。可見,輸入事件有著比顯示螢幕更高的保真度。

如果一連串 touchmove 這樣的事件以每秒 120 次的頻率傳送往主執行緒,那麼可能會觸發過量的命中檢測及 JavaScript 指令碼執行。相形而言,我們的螢幕重新整理率則低下得多。

unfiltered events

圖 7:大量事件湧入合成幀時間軸會造成頁面閃爍

為了降低往主執行緒中傳遞過量呼叫,Chrome 會合並這些連續事件(如:wheel, mousewheel, mousemove, pointermove, touchmove 等),並將其延遲至下一次 requestAnimationFrame 前傳送。

coalesced events

圖 8:相同的時間軸下事件被合併且延遲傳送

所有獨立的事件,如: keydown, keyup, mouseup, mousedown, touchstart, 及 touchend 則會立即發往主執行緒。

使用 getCoalescedEvents 獲取幀內事件

事件合併可幫助大多數 web 應用構建良好的使用者體驗。然而,如果你開發的是一個繪圖類應用,需要基於 touchmove 事件的座標繪製線路,那麼在你試圖畫下一根光滑的線條時,區間內的一些座標點也可能會因事件合併而丟失。這時,你可以使用目標事件的 getCoalescedEvents 方法獲取事件合併後的資訊。

getCoalescedEvents

圖 9:左為流暢的觸控手勢路徑、右為事件合併後的有限路徑

window.addEventListener('pointermove', event => {
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // 使用 x、y 座標畫線
    }
});
複製程式碼

後續步驟

本系列文章裡,我們探討了很多關於 web 瀏覽器內部的工作原理。如果之前你從來沒想過:為什麼 Devtools 推薦在事件處理器上新增 {passive:true} 選項、為什麼有時須在 script 標籤裡新增 async 屬性?那麼我希望這一系列文章能幫助你瞭解,為什麼傳遞這些資訊給瀏覽器能讓其提供更為迅捷流暢的 web 體驗。

使用 Lighthouse

如果你想構建出對瀏覽器更為友好的程式碼,卻一直毫無頭緒,那麼不妨先從使用 Lighthouse 開始。Lighthouse 是個可以幫助你審查網站工具,並且能提供頁面效能報告。效能報告會告訴你什麼地方處理得當,什麼地方有待提升。瀏覽審查列表也能讓你瞭解瀏覽器著力關注的重點所在。

學習如何評測效能

對於不同的站點,桎梏其效能之處可能不盡相同,所以專門為你自己的站點定製化一套效能評測方案,並擇優選取技術應用,是重中之重。Chrome 的 Devtools 團隊就 如何測試你的站點效能 撰有相關教程可供參閱。

為你的站點新增 Feature Policy

如果你想進一步採用更多方案,Feature Policy 是一個新的 web 平臺,可在開發時為你保駕護航。開啟 feature policy 可以限制應用行為,並使你遠離諸多技術弊端。舉個例子,如果你想確保應用不會阻塞解析,那麼可以採用同步指令碼方案執行應用。開啟 sync-script:'none' 後,導致解析阻塞的 JavaScript 指令碼會被阻止執行。這就確保了你的程式碼不會阻塞解析,瀏覽器也無須考慮暫停執行解析器。

總結

thank you

剛踏上開發之路時,我幾乎只關注怎樣去寫程式碼、怎樣提升自己的生產效率。誠然,這些事情很重要,但與此同時我們也應當思考瀏覽器會怎麼去處理我們書寫的程式碼。現代瀏覽器一直致力探索如何提供更好的使用者體驗。書寫對瀏覽器友好的程式碼,反過來也能提供友好的使用者體驗。路漫漫其修遠兮,希望我們能攜手共進,構建出對瀏覽器更為友好的程式碼。

在此筆者誠摯感謝 Alex RussellPaul IrishMeggin KearneyEric BidelmanMathias BynenesAddy OsmaniKinuko YasudaNasko Oskov 和 Charlie Reis 等人對本系列文章初稿的校對。

你喜歡這一系列的文章嗎?如對之後文章有任何意見或建議,歡迎在下面評論區或是推特 @kosamari 裡留下您的寶貴意見。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章