一次useEffect引發瀏覽器執行機制的思考

語言: CN / TW / HK
ead>

丟擲"問題"

我們先來闡述闡述問題,今兒在寫一個有關於新手指引的公用元件,類似於這樣的形式:

image.png

我相信大家首先想到的思路就是在useEffect中通過getBoundingClientRect()獲得對應傳入元素(id)的位置,然後通過定位增加一個類似的彈窗效果。

當我天真的以為這樣就可以實現它的時,我碰到了一個"無從下手"解決的問題。

useEffect中獲取getBoundingClientRect()的值是隨機的?

隨機的???作為一個基本的程式設計師,隨機的程式碼執行結果,這我怎麼能夠接受呢!

我們來看看簡化後的程式碼:

"問題"程式碼

```ts // 程式碼已經是很簡化的版本了 僅僅保留了核心的內容 import React, { useEffect } from 'react' import './_index.scss'

const GuideBeta = () => { useEffect(() => { console.log(document.getElementById('step1')) console.log(document.getElementById('step1')?.getBoundingClientRect()) }, []) return (

第一個指引
第二個指引
) }

export { GuideBeta } ```

上面程式碼其實很簡單,渲染兩個idstep1step2的元素,然後在useEffect()之中去列印獲取idstep1的元素。

差不多頁面渲染出來就是這個樣子:

image.png

輸出結果

這個是正常的輸出結果:

image.png

當時當我們嘗試多重新整理幾次頁面來看看列印結果:

image.png

也許你會奇怪是不是我程式碼寫的有問題,這裡先賣個小關子兩次不同的列印結果,產生的原因和業務程式碼沒有任何關係

要搞清楚這個問題,我們需要從一些基礎的理論知識來層層遞進。

血與淚的教訓,我checked了我的程式碼整整一早上...

瀏覽器載入機制

關於瀏覽器載入機制其實我相信大家已經老生常談了,這裡我結合上邊兩次不同列印的原理來稍微聊聊對應的機制:

js執行瀏覽器會被js引擎"霸佔",從而導致渲染程序無法執行阻塞DomTree的渲染的,那麼Css呢?css載入是否會阻塞Dom Tree的渲染呢?

讓我們帶著這個問題來談談css是否會阻塞Dom Tree的構建。

css載入是否會阻塞Dom Tree的渲染和解析

驗證css載入和Dom Tree的關係

我們嘗試先來看看這端程式碼:

```html

Document

大大的標題

```

程式碼其實很簡單,就是在js指令碼中定時器中獲取h1標籤。之後引入了bootstrap樣式庫。

注意:我們需要將瀏覽器中"網路"限制為SLOW 3G進行測試。

Filmage-2021-10-11_193844.gif

通過上邊的表現,我們可以看到當頁面載入中。js指令碼中的setTimeout已經成功的在控制檯打印出來了h1標籤對應的元素。

也就是說 css還未載入完成,我們就已經可以獲取到對應的Dom,

所以 css載入並不會阻塞Dom Tree的構建。

但是同時注意到,當css檔案載入完成後頁面才會渲染出來藍色的大大的標題,也就是說css檔案載入完成後,頁面才會進行渲染。

此時我們可以得知css的載入是會阻塞Render Tree的渲染的,你可以暫時理解成Render TreeDom Tree,之後我們會在後邊詳細講解。

css對於Dom Tree結論

我們來談談關於css載入的結論:

  1. css載入並不會阻塞Dom Tree的構建,因為css還未載入完時我們已經可以獲取到對應的h1標籤了。
  2. css載入會阻塞Dom Tree的渲染,只有當css載入完成後頁面才會渲染出藍色的大大的標題

css載入對於js的影響

那麼css載入對於js的是否有影響呢?廢話不多說我們來看程式碼:

css載入對於js驗證

```html

Document


大大的標題

```

我們先來看看這段程式碼執行結果,同樣是在SLOW 3G情況下:

Filmage-2021-10-11_201815.gif

我們可以看到兩次指令碼相差2550ms,正好是css程式碼載入完畢之後才開始執行了後邊的script指令碼。

css載入對於js的結論

同樣我們得知,位於css程式碼之前的js程式碼載入執行是毫無疑問的,但位於css載入之後的程式碼,css程式碼的載入是會阻塞後續js程式碼的執行的

css載入結論

我們稍微來總結一下目前關於css載入的結論:

  1. css程式碼載入並不會阻塞Dom Tree的構建。
  2. css程式碼載入是會阻塞Dom Tree在瀏覽器上的渲染。
  3. css程式碼載入是會阻塞後續js程式碼的執行。

造成css載入的原理

上邊我們已經總結過了css載入對於Dom TreejsRender Tree(Dom Tree在瀏覽器上的渲染)部分的表現和總結,現在我們來看看造成這一切的原因:

image.png

一次瀏覽器的渲染流程大概就是如此,關於layout->paint->composite關鍵渲染幀涉及到一些重塑和迴流的知識這部分內容之後我會詳細為大家介紹。

我們先來關注下HTMLCss的載入其實他們是並行載入,這也就印證了我們上邊提到的css載入並不會影響Dom Tree的構建。

但是我們可以看到,當cssomdom tree家在完成後會合併成為一個Render Tree,瀏覽器會根據Render Tree的元素和佈局進行渲染,這也就是我們上邊說到的等到css檔案載入成功後瀏覽器才會渲染出內容

同時瀏覽器的渲染引擎和js的解釋引擎他們是互斥的,也就是說css載入和dom載入都會和js執行載入互斥的。(當然排除scirpt標籤上的deferasync)屬性。

相關瀏覽器載入原理部分大概就提到這裡,我相信結合實際出發去讀原理才會讓人印象深刻。結合上兩個Demo例項我相信大家已經能很好的拿原理思路來佐證我們的結論。

接下來讓我們迴歸文章開頭的問題,來一探究竟:

回到問題本身

針對為什麼我們在useEffect中獲取到的Dom元素是正常的,但是列印getBoundingClientRect()的值卻可能會出現兩種結果呢?

看到這裡我相信你已經能大概猜出來結果,沒錯!他和我們的業務程式碼沒有一毛錢關係,完全取決於css檔案的載入!!(真的是坑慘我了😭)

偶發正常情況分析

我們先來看看當值列印正常時候的net work控制面板:

image.png

image.png

  • console.js是我們react程式碼,包含對應業務邏輯。
  • console.css是我們業務的css程式碼,包含對應的元素位置定義。

我們可以看到,我們的css程式碼是遠遠早與js程式碼載入完成的,也就是說在js程式碼執行之前頁面其實就已經正常渲染了(cssomdomTree合成正確的render Tree),所以此時我們通過useEffect執行完畢拿到的就是正確的位置getBoundingClientRect()

偶發非正常情況分析

我們來看看偶發非正常getBoundingClientRect列印的結果:

image.png

要解釋清楚這個問題,我們首先來看看htmljs檔案和css檔案的順序:

image.png

這是html中的head標籤中載入兩個指令碼的順序,js檔案引用了defer屬性。

所謂defer意思是說js的載入會非同步執行並不會阻塞後續載入,按照載入順序在文件完成解析後,DomContentLoaded事件前依次執行對應載入完成的js指令碼。有關defer詳細資訊你可以在這裡看到

所謂的DomContentLoaded事件,當初始的 HTML 文件被完全載入和解析完成之後,DOMContentLoaded 事件被觸發,而無需等待樣式表、影象和子框架的完全載入。你可以理解成為當Dom Tree構建完成後就會觸發DomContentLoaded事件。

此時也就是說我們的script指令碼會非同步載入等待Dom Tree解析完畢後,DOMContentLoaded事件呼叫前進行執行。

此時我們來看看對應的網路請求結果:

image.png

是我們的js載入快於css載入13ms完成。當js載入完成後css還在請求download中,此時由於dom Tree已經構建完畢符合我們js的執行時機,所以此時js優先於css執行完成。當我們執行js時頁面上並不存在任何樣式,此時我們通過getBoundingClientRect獲取的值自然是不正確的(其實獲取的就是不存在樣式時候的位置值)。

由於defer指令碼已經完成,所以在css載入過程中其實執行緒是空虛的,所以此時js引擎會執行載入完成的defer指令碼進行執行。造成js提前與css執行完畢。

解決方法

其實解決方式存在很多種,這裡我個人選擇使用MutationObserver當監聽Dom元素的屬性(樣式變化時)重新呼叫一次獲取最新位置。

當然你也可以有自己的方式,清楚了問題的本質後有很多種方法都可以實現。

總結

我們來稍微階段性總結一下:

  1. css的載入是會阻塞後續js的執行的,後續js會等待css載入完成後才會執行。
  2. css的載入並不會阻塞Dom Tree的構建。
  3. css的載入是會阻塞頁面渲染的,因為頁面渲染的Render Tree是需要css omdom tree進行合併從而渲染頁面的。

Tips:

關於第二點,css的載入並不會阻塞Dom Tree的構建,但是如果在css檔案之後存在js指令碼,js是會阻塞dom tree的構建的,因為css載入阻塞了js執行,所以間接的阻塞了dom tree的構建。

同時在不同瀏覽器下可能會有不同的解釋機制,這裡絕大多數情況下是針對於chrome進行的解釋。

文章中由於業務引發的"血案"就到此為止了,我們已經闡述了對應發生的機制以及why to do

當然瀏覽器執行機制我相信文章的講述還是比較片面,如果有興趣我們可以在評論區互相交流。