ResizeObserver 可在元素大小變更時通知您。
在 ResizeObserver 之前,您必須將監聽器附加至文件的 resize 事件,才能在視埠尺寸有任何變更時收到通知。在事件處理常式中,您必須找出受該變更影響的元素,並呼叫特定常式來適當反應。如果您需要調整大小後元素的全新尺寸,就必須呼叫 getBoundingClientRect() 或 getComputedStyle(),但如果您未妥善批次處理所有讀取和所有寫入作業,可能會導致版面配置顛簸。
這甚至未涵蓋元素在主視窗未調整大小的情況下變更大小的情況。舉例來說,附加新子項、將元素的 display 樣式設為 none,或類似動作可能會變更元素、同層級元素或祖先元素的大小。
這就是 ResizeObserver 實用的原因。無論變更原因為何,只要觀察的任何元素大小發生變化,這個 Hook 就會做出反應。您也可以存取所觀察元素的新大小。
API
上述所有帶有 Observer 後置字元的 API 都採用簡單的 API 設計。ResizeObserver 也不例外。您會建立 ResizeObserver 物件,並將回呼傳遞至建構函式。回呼會傳遞 ResizeObserverEntry 物件的陣列 (每個觀察到的元素各有一個項目),其中包含元素的新尺寸。
var ro = new ResizeObserver(entries => {
for (let entry of entries) {
const cr = entry.contentRect;
console.log('Element:', entry.target);
console.log(`Element size: ${cr.width}px x ${cr.height}px`);
console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
}
});
// Observe one or multiple elements
ro.observe(someElement);
詳細資料
系統會回報哪些資料?
一般來說,ResizeObserverEntry 會透過名為 contentRect 的屬性回報元素的內容方塊,並傳回 DOMRectReadOnly 物件。內容方塊是可放置內容的方塊。這是指邊框方塊減去邊框間距。
請注意,雖然 ResizeObserver reports contentRect 和邊框間距的維度,但只會 watches contentRect。請勿將 contentRect 與元素的定界框混淆。getBoundingClientRect() 回報的邊界方塊是包含整個元素及其子系的方塊。SVG 是這項規則的例外狀況,其中 ResizeObserver 會回報定界框的尺寸。
自 Chrome 84 起,ResizeObserverEntry 新增了三項屬性,可提供更詳細的資訊。這些屬性都會傳回 ResizeObserverSize 物件,其中包含 blockSize 屬性和 inlineSize 屬性。這項資訊與回呼叫用時觀察到的元素有關。
borderBoxSizecontentBoxSizedevicePixelContentBoxSize
所有這些項目都會傳回唯讀陣列,因為我們希望日後這些項目能支援具有多個片段的元素,這類元素會出現在多欄情境中。目前這些陣列只會包含一個元素。
這些屬性的平台支援有限,但 Firefox 已支援前兩項。
何時回報?
規格規定 ResizeObserver 應在繪製前和版面配置後處理所有大小調整事件。因此,ResizeObserver 的回呼是變更網頁版面配置的理想位置。由於 ResizeObserver 處理作業會在版面配置和繪製之間進行,因此這麼做只會使版面配置失效,不會使繪製失效。
Gotcha
您可能會想問:如果我在回呼中將受監控元素的大小變更為 ResizeObserver,會發生什麼事?答案是:您會立即觸發對回呼的另一次呼叫。幸好 ResizeObserver 具有避免無限回呼迴圈和循環依附元件的機制。只有在重新調整大小的元素位於 DOM 樹狀結構中,比前一個回呼中處理的最淺元素更深時,系統才會在同一個影格中處理變更。否則,系統會將其延後至下一個影格。
應用程式
ResizeObserver 的其中一項功能是實作每個元素的媒體查詢。觀察元素後,您就能強制定義設計中斷點,並變更元素的樣式。在下列範例中,第二個方塊會根據寬度變更邊框半徑。
const ro = new ResizeObserver(entries => {
for (let entry of entries) {
entry.target.style.borderRadius =
Math.max(0, 250 - entry.contentRect.width) + 'px';
}
});
// Only observe the second box
ro.observe(document.querySelector('.box:nth-child(2)'));
另一個值得參考的有趣範例是聊天視窗。在一般的由上而下對話版面配置中,會出現捲動位置的問題。為避免使用者混淆,建議將視窗固定在對話底部,也就是顯示最新訊息的位置。此外,任何版面配置變更 (例如手機從橫向切換為直向,反之亦然) 也應達到相同效果。
ResizeObserver 可讓您編寫單一程式碼,同時處理這兩種情況。根據定義,ResizeObserver 可以擷取視窗大小調整事件,但呼叫 appendChild() 也會調整該元素的大小 (除非設定 overflow: hidden),因為需要為新元素騰出空間。因此,您只需要幾行程式碼,就能達到所需效果:
const ro = new ResizeObserver(entries => {
document.scrollingElement.scrollTop =
document.scrollingElement.scrollHeight;
});
// Observe the scrollingElement for when the window gets resized
ro.observe(document.scrollingElement);
// Observe the timeline to process new messages
ro.observe(timeline);
很酷吧?
從這裡開始,我可以新增更多程式碼來處理使用者手動向上捲動,並希望在收到新訊息時,捲動位置能停留在該訊息的情況。
另一種用途是處理自行排版的任何自訂元素。直到 ResizeObserver,如果尺寸變更,就沒有可靠的方式可取得通知,以便重新配置子項。
對 Interaction to Next Paint (INP) 的影響
Interaction to Next Paint (INP) 指標可評估網頁回應使用者互動的整體效能。如果網頁的 INP 處於「良好」門檻 (也就是 200 毫秒或更短),表示網頁能可靠地回應使用者與網頁的互動。
雖然事件回呼回應使用者互動所需的時間,可能會大幅影響互動的總延遲時間,但這並非影響 INP 的唯一因素。INP 也會考量互動的下一次繪製所需時間。這是指完成算繪作業所需的時間,以便在使用者互動後更新使用者介面。
就 ResizeObserver 而言,這點非常重要,因為 ResizerObserver 執行個體執行的回呼正好發生在算繪作業之前。這是設計使然,因為回呼中發生的工作必須納入考量,因為該工作的結果很可能需要變更使用者介面。
請注意,在 ResizeObserver 回呼中,盡量只執行必要算繪工作,因為過多的算繪工作可能會導致瀏覽器延遲執行重要工作。舉例來說,如果任何互動的回呼會導致 ResizeObserver 回呼執行,請務必執行下列操作,盡可能提供流暢的體驗:
- 請盡可能簡化 CSS 選擇器,避免過度重新計算樣式。樣式重新計算會在版面配置前進行,複雜的 CSS 選取器可能會延遲版面配置作業。
- 請避免在
ResizeObserver回呼中執行任何可能觸發強制重排的工作。 - 一般來說,網頁上的 DOM 元素越多,更新網頁版面配置所需的時間就越長。無論網頁是否使用
ResizeObserver,這都是事實,但隨著網頁結構複雜度增加,ResizeObserver回呼中完成的工作可能會變得相當重要。
結論
ResizeObserver 適用於所有主要瀏覽器,可有效監控元素層級的元素大小調整作業。但請注意,別使用這個強大的 API 過度延遲算繪作業。