你的 onscroll 不是你的 onscroll

前陣子同事處理滾動事件時,發現了 window.onscrolldocument.body.onscroll 連動的奇妙現象。我覺得很有趣,便嘗試釐清背後的原因,整理後跟大家分享。

這篇文章偏向以流水帳方式紀錄我追蹤這個問題的過程,因此可能略顯冗長。若趕時間的話可以跳到文末的結論觀看。

先備知識 🍘

有兩種方法可以監聽 DOM 事件:

  1. addEventListener()
  2. onevent handler

而後者主要可透過兩種方法註冊:

  • 直接在 element tag 上面加 HTML attribute。例如:
<button onclick="doSomeCoolStuff()">Click me</button>
  • 透過 JavaScript 選到元素後,置換它的 event handler。例如:
const coolButton = document.querySelector('button');
coolButton.onclick = function(event) {
// Do some cool stuff here
};

其他值得一提的還有:

  • 同一種事件只能有一個 onevent handler。重複寫的話,後面寫的會覆蓋前面的。例如:
  • 如果用 addEventListener() 註冊事件的話,同一種事件可以有很多個 handler,而且彼此不會覆蓋。例如:

本篇文章聚焦在使用 onevent ——特別是 onscroll ——時所發生的特殊現象。

關於 onscroll 的奇妙現象

動動腦時間

假設頁面一開始沒有綁定任何事件,請問下列程式碼會分別在 console 顯示什麼訊息?

⚡️

第一題答案

console.log(window.onscroll); 會顯示:

ƒ handleScroll() {
console.log('window.onscroll');
}

因為我們一開始就置換了 window.onscroll,所以理所當然地,console 會把 handleScroll 秀出來。

第二題答案

console.log(document.onscroll); 會顯示:

null

由於我們從未置換 document.onscroll,頁面也是乾淨的,就算要猜的話,應該會猜 undefinednull,對吧?

至此都算合理,直到⋯⋯

第三題答案

console.log(document.body.onscroll); 會顯示:

ƒ handleScroll() {
console.log('window.onscroll');
}

我們從頭到尾都沒碰 document.body.onscroll,但它竟然不是預設值 null

等等,那如果反過來操作,先置換 document.body.onscroll,再去看 window.onscroll 呢?

聰明的各位現在應該能猜到 console 的訊息:

ƒ handleScroll() {
console.log('document.body.onscroll');
}

謎之現象

看來,window.onscroll 似乎和 document.body.onscroll 是連動的,改了其中一個,另一個就會跟著變動。

那其他事件呢,該不會其實也都存在類似現象吧?

看起來 onclick 沒有這種連動的情況⋯⋯

到底怎麼一回事呢?

我試著查了 Stack Overflow,但網友的回答通常是提供解決方法(例如建議發問者改用 addEventListener() 去監聽滾動事件),並沒有闡述造成類似現象背後的理由。而 MDN 上面我一時也找不到相關的說明。

沒辦法,只好硬著頭皮讀 HTML Specification 了。

我先搜尋「event handler」,發現這個頁面有列出各種 event handler,其中有幾種非常特別,甚至有自己獨特的稱呼。

╔═══════════════╦════════════╗
║ Event Handler ║ Event Type ║
╠═══════════════╬════════════╣
║ onblur ║ blur ║
║ onerror ║ error ║
║ onfocus ║ focus ║
║ onload ║ load ║
║ onresize ║ resize ║
║ onscroll ║ scroll ║
╚═══════════════╩════════════╝

We call the set of the names of the event handlers listed in the first column of this table the Window-reflecting body element event handler set.

看名字大概就能猜到一些端倪 😆

接著我去追查了這個酷酷的名詞(Window-reflecting body element event handler set),發現在講 body 的頁面提到:

The body element exposes as event handler content attributes a number of the event handlers of the Window object. It also mirrors their event handler IDL attributes.

The event handlers of the Window object named by the Window-reflecting body element event handler set, exposed on the body element, replace the generic event handlers with the same names normally supported by HTML elements.

真相大白。

結論

關於 HTML Specification 的說明,我的理解如下:

  • 我們可透過 document.body.onxxx<body onxxx=""> 去存取/置換 Window 的某些 event handler
  • onbluronerroronfocusonloadonresizeonscroll,稱作 Window-reflecting body element event handler set
  • 當我們在 body 存取/置換 Window-reflecting body element event handler set 時,我們存取/置換的其實不是 body 的 event handler,而是 Window 的 event handler

如此一來 window.onscrolldocument.body.onscroll 的關係也明瞭了:

  • 由於 document.body.onscroll 指向的是 window.onscroll,因此當我們置換前者時,我們置換的其實是後者
  • 當我們置換 window.onscroll 後,document.body.onscroll 會反映前者的變動,因此 document.body.onscroll 也跟著改變

後記

我在整理參考資料時,發現 MDN 其實有提到相關情形,不過它只列了五種 event handler(少了 onresize):

For historical reasons, some attributes/properties on the <body> and <frameset> elements instead set event handlers on their parent Window object. (The HTML specification names these: onblur, onerror, onfocus, onload, and onscroll.)

--

--

2018 年 8 月開始接觸網頁前端開發,喜歡寫跟生活有關的東西。

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store