自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Resize Observer 介紹及原理淺析

開發(fā) 前端
響應式設(shè)計如今也成為 web 應用的基本需求,而現(xiàn)在很多 web 應用都已經(jīng)組件化,這意味著我們?nèi)绻胍獙崿F(xiàn)響應式的應用,那么我們也需要有某種方式監(jiān)聽 「組件/元素」 大小的變化,以便讓 「組件/元素」 也做到響應式。

背景?

響應式設(shè)計指的是根據(jù)屏幕視口尺寸的不同,對 Web 頁面的布局、外觀進行調(diào)整,以便更加有效地進行信息的展示。我們?nèi)粘I钪薪佑|的很多應用都遵循響應式的設(shè)計。

響應式設(shè)計如今也成為 web 應用的基本需求,而現(xiàn)在很多 web 應用都已經(jīng)組件化,這意味著我們?nèi)绻胍獙崿F(xiàn)響應式的應用,那么我們也需要有某種方式監(jiān)聽 「組件/元素」 大小的變化,以便讓 「組件/元素」 也做到響應式。

在 ResizeObserver 出現(xiàn)之前,我們也有一些實現(xiàn)響應式布局的方案,包括:

  • JS 方案——window.onresize /window.matchMedia。
  • CSS 方案——媒體查詢。

但它們都各自有一些問題。

media query 媒體查詢 - CSS 方案

在 CSS 中可以通過媒體查詢實現(xiàn)響應式,但 CSS 的媒體查詢只能監(jiān)聽全局屬性,比如 viewport 的大小、screen 的大小等,并不能監(jiān)聽元素級別的尺寸變化。

而即使 CSS 能夠?qū)υ丶墑e進行監(jiān)聽,也會遇到循環(huán)引用問題,舉個例子,假設(shè)我們能夠?qū)δ硞€具體元素的寬度進行監(jiān)聽,并寫出了以下代碼: (注意現(xiàn)在并不支持 :min-width 這樣的偽類寫法,下面只是偽代碼)

.father {
float: left;
}
.child {
width: 500px;
}
.father:min-width(450px) > .child {
width: 400px;
}
  • 因為.father 設(shè)置了float: left ,所以它的寬度由 子元素 child 的寬度來決定,即一開始時為 500px;
  • 如果.father 的寬度為 500px (大于 450px ),那么按照最后一個選擇器的寫法,子元素寬度應該變?yōu)?400px;但當子元素寬度為 400px 時,也會使得外層 father 的寬度變?yōu)?400px;
  • 因此子元素寬度又會變?yōu)?500px,此時循環(huán)引用便開始了....

window.resize - JS 方案

resize 事件只有當 viewport 的大小發(fā)生變化時會被觸發(fā),元素大小的變化不會觸發(fā) resize 事件;并且也只有注冊在 window 對象上的回調(diào)會在 resize 事件發(fā)生時被調(diào)用,其他元素上的回調(diào)不會被調(diào)用。

當 「resize」 事件發(fā)生后,我們往往需要通過調(diào)用 getBoundingClientRect? 或者 getComputedStyle? 來獲取此時我們關(guān)心的元素大小,以此判斷元素是否發(fā)生了變化。頻繁調(diào)用 getBoundingClientRect? 、 getComputedStyle等 API 會導致 「瀏覽器重排(reflow)」,導致頁面性能變差,舉個例子:https://codesandbox.io/s/resize-event-5qn3q0?file=/index.html。

調(diào)用 getBoundingClientRect 等函數(shù)時,瀏覽器為了保證我們拿到的元素參數(shù)是準確的,會觸發(fā)一次 reflow 來重新布局。頻繁地調(diào)用以上函數(shù)就會導致瀏覽器頻繁重排、重繪,進而導致性能問題的出現(xiàn)。

雖然我們可以通過合并讀/寫操作,或是采用節(jié)流防抖,來減少重繪的次數(shù),但不可避免的,我們至少需要額外調(diào)用至少一次 getBoundingClientRect 操作。

而且當 viewport 大小不變,元素大小變化時,此時我們不能通過監(jiān)聽 resize 事件來得知這一變化。比如在元素下 append 了一個新的 children,或者將元素的 display? 設(shè)為 none,亦或是改變該元素父級節(jié)點或是相鄰節(jié)點的大小,以上這些都有可能在 viewport 大小不發(fā)生變化的情況下,導致元素大小改變,而此時通過監(jiān)聽 「resize」 事件我們就沒辦法感知到這些變化。

window.matchMedia - JS 方案

可以把 matchMedia 理解為 CSS 中媒體查詢的JS方案。

和 window.resize 類似,window.matchMedia 也只能監(jiān)聽 viewport 大小的變化;但和 window.resize 會在每次 viewport 大小變化時都觸發(fā)事件不同,matchMedia 關(guān)心的是某些特殊的斷點,這往往更符合我們實現(xiàn)響應式網(wǎng)頁的實際場景。

舉個例子,我們想實現(xiàn)在屏幕寬度小于 1080px 時將三列布局改為兩列布局,我們并不希望每次 window 大小變化時通知我們 ,而只希望屏幕在大于或小于某個特定的大小時通知我們即可。這種場景下使用 matchMedia 會比監(jiān)聽 window.resize 要性能更高。

const m = matchMedia('(max-width: 600px)')
m.addEventListener('change',(event)=>{console.log('macth onChange', event)})

小結(jié)

方案

相同問題

特殊問題

Media query-CSS

只能監(jiān)聽viewport變化,不能監(jiān)聽某個 「組件/元素」 大小變化

循環(huán)引用問題

window.resize-JS

需要在 viewport 大小變化時手動獲取元素的大小,可能導致性能問題

window-matchMedia-JS

以上提到的三種瀏覽器原生方案都存在著只能監(jiān)聽 viewport 大小變化,而不能監(jiān)聽 「組件/元素」 大小變化的問題。此外,CSS 的媒體查詢存在著循環(huán)引用的問題,window.onresize? 和 window.matchMedia 則都需要在 viewport 大小變化時手動獲取元素的大小,一旦操作過于頻繁則可能導致瀏覽器多次 reflow。

ResizeObserver 就是為了解決以上問題而出現(xiàn)的,可以將其理解為 window.onresize? 的「組件/元素級別」 的替代方案。使用 ResizeObserver 可以讓我們監(jiān)聽到元素大小的變化,無需再手動調(diào)用 getBoundingClientRect 來獲取元素的尺寸大小,同時也解決了無限回調(diào)和循環(huán)依賴的問題。

ResizeObserver的使用?

API

  • ResizeObserver.disconnect:取消和結(jié)束目標對象上所有對 Element 或 SVGElement 觀察。
  • ResizeObserver.observe:開始觀察指定的 Element 或 SVGElement。
  • 第一個參數(shù)為觀察的元素。
  • 第二個參數(shù)為可選參數(shù) BoxOptions,用來指定將以哪種盒子模型來觀察變動,如content-box (默認值),border-box和device-pixel-content-box。
  • ResizeObserver.unobserve:結(jié)束觀察指定的 Element 或 SVGElement。
const 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);

附上 MDN 的兩個demo:

  • Resize observer border-radius test - CodeSandbox:https://codesandbox.io/s/resize-observer-border-radius-test-ztwuyg。
  • Resize observer text test - CodeSandbox:https://codesandbox.io/s/resize-observer-text-test-dktwk1。

什么時候觸發(fā)通知

與我們關(guān)注的盒模型有關(guān),ResizeObserver 會根據(jù)調(diào)用 observe 函數(shù)時傳遞的第二個可選參數(shù) BoxOptions 傳入的盒模型參數(shù)進行監(jiān)聽,當元素該盒模型變化時觸發(fā)通知。默認監(jiān)聽 content-box變化以觸發(fā)監(jiān)聽。

通知內(nèi)容包括什么

通知的內(nèi)容包含了足夠的信息,以便開發(fā)者能夠根據(jù)當前元素的具體大小信息來作出變化,而不是要開發(fā)者重新調(diào)用 getComputedStyle、 getBoundingClientRect 來獲取。

  • 監(jiān)聽元素:target。
  • contentRect。
  • contentBoxSize。
  • borderBoxSize。
  • devicePixelContentBoxSize。

需要注意的是,雖然只有當 BoxOptions 關(guān)心的盒模型變化時才會觸發(fā)通知,但實際上通知時會將三種不同盒模型下的具體大小都返回給回調(diào)函數(shù),用戶無需再次手動獲取。

在 React 中使用

為了避免在 React render中多次聲明 ResizeObserver 實例,我們可以把實例化過程放在 useLayoutEffect 或 useEffect 中。并且在非 SSR 場景中,我們應該盡量使用 useLayoutEffect 而不是 useEffect。

useLayoutEffect 和 useEffect 的最大差別在于執(zhí)行時機的不同,useEffect 會在瀏覽器繪制完成之后調(diào)用,而 useLayoutEffect 則會在 React 更新 dom 之后,瀏覽器繪制之前執(zhí)行,并且會阻塞后面的繪制過程,因此適合在 useLayoutEffect 中進行更改布局、及時獲取最新布局信息等操作。

ResizeObserver 原理?

執(zhí)行時機

圖片

先從瀏覽器渲染流程開始說起,網(wǎng)頁渲染會經(jīng)歷以下幾個主要過程:

  1. 解析 HTML,構(gòu)建 DOM 樹。
  2. 解析 CSS,生成 CSS 規(guī)則樹。
  3. 布局 Layout——合并 DOM 樹和 CSS 規(guī)則,生成 Render 樹。
  4. 繪制 Paint——繪制 Render 樹(paint),繪制頁面像素信息。

「如果是由我們來設(shè)計,我們應該在以上渲染流程中的哪個環(huán)境來執(zhí)行 ResizeObserver 的監(jiān)聽通知會比較合理?」

因為我們在 ResizeObserver 的回調(diào)函數(shù)中可以(也經(jīng)常會)根據(jù)當前元素的大小來改變 style 或者 dom 樹,而這些操作往往都會觸發(fā) layout/reflow;因此,應該是在 「布局Layout 和 繪制Paint 之間」來執(zhí)行回調(diào)函數(shù)會更加合理。

而如果有多個 ResizeObserver 實例都在回調(diào)中進行了改變布局的操作,那么最好的方式就是在所有回調(diào)都執(zhí)行完重新布局,確保得到一個最終準確的布局之后,再來進行繪制 Paint,避免繪制的內(nèi)容是無效內(nèi)容。

因此如上圖所示,ResizeObserver 的通知會在 Layout 和 Paint 之間進行(圖中的 4 Notify),當回調(diào)中改變了 Layout 時,則會重新 loop 執(zhí)行 Animate、RAF、Layout、Notify,直到所有需要被通知的元素都通知完(也可以理解為 loop循環(huán) 會在 layout 不再被改變時結(jié)束)。

如何判斷是否需要通知

每個 ResizeObserver 實例內(nèi)都有一個 ResizeObservation 對象,ResizeObservation 對象表達了一種訂閱監(jiān)聽的關(guān)系,并在其中記錄了監(jiān)聽的元素(target)、監(jiān)聽的盒模型(即observe函數(shù)的第二個參數(shù))、上次通知的值(lastReportedSizes,即上次通知時元素的大小尺寸)

每次 layout 過后,對于監(jiān)聽的每個元素,都會重新計算元素的大小,并與上次通知的大小(lastReportedSizes)進行比較,一旦大小發(fā)生變化才會被設(shè)置為 active,意味著 「可能」 會被通知。為什么這里提的是 「可能」 ,下面會進行解釋。

需要注意的是,內(nèi)部獲取元素的大小是通過調(diào)用 getComputedStyle 實現(xiàn)的,那么多次調(diào)用 getComputedStyle 會不會導致瀏覽器頻繁 layout/reflow ?

  • 在瀏覽器觸發(fā) reflow 后,所有已有元素位置都會記錄快照,只要不再觸發(fā)位置等變化導致快照失效,那么第二次開始訪問位置就不會觸發(fā) reflow。
  • 當前面的通知回調(diào)改變了 Layout 時,下一個 ResizeObserver 實例調(diào)用 getComputedStyle 時就有可能導致瀏覽器 reflow。
  • 但此時為了獲取準確的元素信息, reflow 是無法避免的;因為不涉及到 繪制paint,所以開銷還是可接受的。

無限循環(huán)

圖片

結(jié)合上圖,我們假設(shè)這樣的場景,在監(jiān)聽到 「節(jié)點1」 寬度變化時,設(shè)置 「子孫節(jié)點2」 的寬度;而在 「節(jié)點2」 寬度改變時,我們對 「節(jié)點1」 的寬度進行改變,此時可能又會觸發(fā) 「節(jié)點1」 的監(jiān)聽回調(diào),從而出現(xiàn)無限循環(huán)的監(jiān)關(guān)系。

在 ResizeObserver 的回調(diào)中對 dom 進行操作,比如改變另外一個元素的大小,或是隱藏/展示某個元素,這些操作可能會導致新的回調(diào)調(diào)用,引發(fā)無限循環(huán),最終導致界面 UI 卡死。上面我們只舉三個層級節(jié)點的例子作為說明,如果節(jié)點監(jiān)聽關(guān)系的數(shù)量越多、層級越深,那么情況就會更糟。

還有另外一種場景是,在監(jiān)聽函數(shù)中創(chuàng)建新的 ResizeObserver 實例,導致循環(huán)的每一次迭代都有新的元素需要通知,那么最終循環(huán)就會因為內(nèi)存溢出而終止,這里不作過多討論。

如果避免無限循環(huán)

無限循環(huán)的場景是真實存在的,想要避免無限循環(huán)的出現(xiàn),我們需要給循環(huán)過程加上一些限制,以此來解除循環(huán)。有三種限制策略可以考慮:

  • 執(zhí)行次數(shù)限制。
  • 允許執(zhí)行最多次數(shù) N 次循環(huán),當超過次數(shù) N 時,循環(huán)終止。
  • 優(yōu)點是實現(xiàn)簡單,并且具有一致性,當這個算法在不同的機器上運行時都能有相同的表現(xiàn)。
  • 缺點是 N 的定義太過隨意,缺乏比較可靠的結(jié)論定義。
  • 執(zhí)行時間限制。
  • 循環(huán)最多執(zhí)行 N ms 時長,當超過這個時間時循環(huán)終止。
  • 雖然聽起來實現(xiàn)很簡單,但我們無法保證具體會執(zhí)行多少次調(diào)度,在不同性能的機器上,每次執(zhí)行的時間是不同的,意味著不同的機器執(zhí)行次數(shù)會不同,也可能因此導致不同機器上最終展示的內(nèi)容不一致。
  • 執(zhí)行深度限制。

執(zhí)行深度限制

設(shè)定一次渲染流程中需要通知的元素(指的是和上次通知時的大小 lastReportedSize 相比發(fā)生了變化)為集合 N,設(shè)定上次迭代的元素最小深度 Depth 為 ∞

當 N 不為空時,開始循環(huán)。

  1. 在一次迭代中,對集合 N 中的所有元素進行通知(并在通知中可能觸發(fā)重新布局流程),并將 Depth 更新為本次迭代中元素的最小深度 d。
  2. 將所有小于等于深度 d 的元素移除,更新集合 N——即下次迭代只會對比上次迭代的最淺元素更深的元素進行通知。

直到 N 為空時,循環(huán)終止,通知結(jié)束,開始瀏覽器繪制 Paint。

圖片

通過以上說明,我們也可以意識到在一次循環(huán)中,只有滿足以下兩個條件的元素才會被通知:

  1. 上次迭代/Layout過后,元素的大小被改變了。
  2. 元素的深度比上次迭代的最淺深度更低。

「那么深度限制就不存在問題了嗎?」

深度限制可能會使得頁面展示不是完全準確的,但是相比于頁面UI卡死,這個問題對于用戶而言是更好接受的。

責任編輯:姜華 來源: Tecvan
相關(guān)推薦

2009-07-07 16:39:40

JDK Observe

2009-07-06 09:23:51

Servlet定義

2023-12-18 09:39:13

PreactHooks狀態(tài)管理

2009-08-13 13:42:54

C#構(gòu)造函數(shù)

2009-09-07 03:37:51

C#窗體

2011-12-20 15:52:03

PhoneGap架構(gòu)基礎(chǔ)工作原理

2011-12-07 14:25:33

JavaNIO

2023-11-16 09:01:37

Hadoop數(shù)據(jù)庫

2020-08-05 08:21:41

Webpack

2017-07-19 11:11:40

CTS漏洞檢測原理淺析

2023-05-11 07:25:57

ReduxMiddleware函數(shù)

2009-07-16 10:23:30

iBATIS工作原理

2020-11-05 11:14:29

Docker底層原理

2009-09-04 10:05:16

C#調(diào)用瀏覽器瀏覽器的原理

2009-07-14 14:28:31

MyEclipse E

2010-09-25 14:01:11

Java跨平臺

2022-06-09 15:53:16

移動端渲染GPU

2009-12-09 14:34:51

PHP生成HTML

2019-08-05 13:20:35

Android繪制代碼

2011-04-13 15:01:39

點贊
收藏

51CTO技術(shù)棧公眾號