1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        一個(gè)你不知道的 useLayoutEffect 的秘密

        共 19635字,需瀏覽 40分鐘

         ·

        2024-03-23 12:30

        ?

        萬(wàn)丈高樓平地起,勿在浮沙筑高臺(tái)

        ?
        前言

        React中針對(duì)DOM操作的最常見(jiàn)方法是使用refs來(lái)訪問(wèn)DOM節(jié)點(diǎn),其實(shí)還有一種方法,就是使用useLayoutEffect來(lái)訪問(wèn)DOM節(jié)點(diǎn),根據(jù)實(shí)際 DOM 測(cè)量(例如元素的大小或位置)來(lái)更改元素。

        今天,我們就來(lái)講講useLayoutEffect如何處理DOM,還有從底層是如何實(shí)現(xiàn)的?

        好了,天不早了,干點(diǎn)正事哇。

        2af32a4d386ff3a96929acdaccad3733.webp

        我們能所學(xué)到的知識(shí)點(diǎn)

        ?
        1. 前置知識(shí)點(diǎn)
        2. useEffect 導(dǎo)致布局閃爍
        3. 使用 useLayoutEffect 修復(fù)閃爍問(wèn)題
        4. 瀏覽器如何渲染頁(yè)面
        5. useEffect vs useLayoutEffect
        6. 在 Next.js 和其他 SSR 框架中使用 useLayoutEffect
        ?

        1. 前置知識(shí)點(diǎn)
        ?

        「前置知識(shí)點(diǎn)」,只是做一個(gè)概念的介紹,不會(huì)做深度解釋。因?yàn)?,這些概念在下面文章中會(huì)有出現(xiàn),為了讓行文更加的順暢,所以將本該在文內(nèi)的概念解釋放到前面來(lái)。「如果大家對(duì)這些概念熟悉,可以直接忽略」

        同時(shí),由于閱讀我文章的群體有很多,所以有些知識(shí)點(diǎn)可能「我視之若珍寶,爾視只如草芥,棄之如敝履」。以下知識(shí)點(diǎn),請(qǐng)「酌情使用」。

        ?

        強(qiáng)制布局

        EventLoop = TaskQueue + RenderQueue有介紹,然后我們?cè)诤?jiǎn)單提一下。

        強(qiáng)制布局(Forced Synchronous LayoutForced Reflow)是Web性能優(yōu)化領(lǐng)域的一個(gè)術(shù)語(yǔ),它指的是瀏覽器在能夠繼續(xù)「處理后續(xù)操作之前,必須完成當(dāng)前的布局計(jì)算」。

        ?

        當(dāng)強(qiáng)制執(zhí)行布局時(shí),瀏覽器會(huì)暫停JS主線程,盡管調(diào)用棧不是空的。

        ?

        有很多我們耳熟能詳?shù)牟僮?,都?huì)觸發(fā)強(qiáng)制布局。

        2e5958b9dae51f5e8ddc2bf13fd2e94b.webp

        其中有我們很熟悉的getBoundingClientRect(),下文中會(huì)有涉及。

        想了解更多??觸發(fā)強(qiáng)制布局的操作[1]。

        阻塞渲染

        在瀏覽器中,阻塞渲染是指當(dāng)瀏覽器在加載網(wǎng)頁(yè)時(shí)遇到阻塞資源(通常是外部資源如樣式表、JavaScript文件或圖像等),它會(huì)停止渲染頁(yè)面的過(guò)程,直到這些資源被下載、解析和執(zhí)行完畢。這種行為會(huì)導(dǎo)致頁(yè)面加載速度變慢,用戶可能會(huì)感覺(jué)到頁(yè)面加載較慢或者出現(xiàn)空白的情況。

        舉例來(lái)說(shuō),如果一個(gè)網(wǎng)頁(yè)中引用了外部的JavaScript文件,并且這個(gè)文件比較大或者加載速度較慢,瀏覽器會(huì)等待這個(gè)JavaScript文件下載完成后才繼續(xù)渲染頁(yè)面,導(dǎo)致頁(yè)面在此過(guò)程中停滯或者出現(xiàn)明顯的加載延遲。

        下面是一個(gè)簡(jiǎn)單的示例,展示了一個(gè)會(huì)阻塞頁(yè)面加載的情況:

              
              <!DOCTYPE html>
        <html>
        <head>
            <title>阻塞渲染示例</title>
            <!-- 假設(shè)這是一個(gè)較大的外部 JavaScript 文件 -->
            <script src="large_script.js"></script>
            <style>
                /* 一些樣式 */
            
        </style>
        </head>
        <body>
            <h1>阻塞渲染示例</h1>
            <!-- 頁(yè)面其余內(nèi)容 -->
        </body>
        </html>

        在這個(gè)示例中,large_script.js 是一個(gè)較大的 JavaScript 文件,它會(huì)阻塞頁(yè)面的加載和渲染。瀏覽器在遇到這個(gè) <script> 標(biāo)簽時(shí)會(huì)暫停頁(yè)面的渲染,直到large_script.js 文件完全下載、解析并執(zhí)行完畢,然后才會(huì)繼續(xù)渲染頁(yè)面的其余內(nèi)容。

        為了減少阻塞渲染對(duì)頁(yè)面加載速度的影響,可以采取一些優(yōu)化策略,比如:

        1. 「異步加載資源」:使用 asyncdefer 屬性加載 JavaScript 文件,讓它們不會(huì)阻塞頁(yè)面渲染。
        2. 「資源合并與壓縮」:將多個(gè)小文件合并為一個(gè)大文件,并對(duì)文件進(jìn)行壓縮,減少下載時(shí)間。
        3. 「延遲加載」:將不是立即需要的資源推遲加載,比如在頁(yè)面滾動(dòng)到特定位置或用戶執(zhí)行某些操作時(shí)再加載。

        2. useEffect 導(dǎo)致布局閃爍

        假設(shè)存在以下場(chǎng)景:有一個(gè)「響應(yīng)式」導(dǎo)航組件,它會(huì)根據(jù)容器的大小來(lái)調(diào)整其子元素的數(shù)量。

        ef9ce505803cf6e06c0161982bda5982.webp

        如果,容器不能容納這些組件,那么它會(huì)在容器的右側(cè)顯示一個(gè)“更多”按鈕,點(diǎn)擊后會(huì)顯示一個(gè)下拉菜單,其中包含剩余未展示的子項(xiàng)目

        ea38e9ce7933a7e0c669adac51f422b0.webp

        讓我們先從簡(jiǎn)單的邏輯入手,先創(chuàng)建一個(gè)簡(jiǎn)單的導(dǎo)航組件,它將呈現(xiàn)一個(gè)鏈接列表:(直接遍歷items來(lái)渲染對(duì)應(yīng)的項(xiàng)目)

              
              const Component = ({ items }) => {
          return (
            <div className="navigation">
              {items.map((item) => (
                <a href={item.href}>{item.name}</a>
              ))}
            </div>

          );
        };

        上面的代碼,只負(fù)責(zé)對(duì)items進(jìn)行遍歷和展示,沒(méi)有任何響應(yīng)式的處理。要想實(shí)現(xiàn)響應(yīng)式,我們需要計(jì)算「可用空間」中可以容納多少個(gè)項(xiàng)目。為此,我們需要知道容器的寬度以及每個(gè)項(xiàng)目的尺寸。并且,我們無(wú)法「未卜先知」其項(xiàng)目中文案信息,也就無(wú)法提前做任何工作,例如通過(guò)計(jì)算每個(gè)項(xiàng)目的文本長(zhǎng)度來(lái)計(jì)算剩余空間。

        既然,我們無(wú)法未雨綢繆,那我們只能亡羊補(bǔ)牢了,也就是我們只有在瀏覽器已經(jīng)把這些項(xiàng)目都渲染出來(lái)后,然后通過(guò)原生 JavaScript API(例如getBoundingClientRect)來(lái)獲取這些項(xiàng)目的尺寸。

        借助 getBoundingClientRect 獲取項(xiàng)目尺寸

        我們需要分幾步來(lái)完成。

        1. 獲取元素的訪問(wèn)權(quán)

        創(chuàng)建一個(gè) Ref 并將其分配給包裝這些項(xiàng)目的 div

              
              const Component = ({ items }) => {
          const ref = useRef(null);

          return (
            <div className="navigation" ref={ref}>
              ...
            </div>

          );
        };

        2. 在 useEffect 中獲取元素的尺寸

              
              const Component = ({ items }) => {

          useEffect(() => {
            const div = ref.current;
            const { width } = div.getBoundingClientRect();
          }, [ref]);

          return ...
        }

        3. 迭代 div 的子元素并將其寬度提取到數(shù)組中

              
              const Component = ({ items }) => {

          useEffect(() => {
            // 與以前相同的代碼

            // 將div的子元素轉(zhuǎn)換為數(shù)組
            const children = [...div.childNodes];
            // 所有子元素的寬度
            const childrenWidths = children.map(child => child.getBoundingClientRect().width)
          }, [ref]);

          return ...
        }

        既然,父容器的寬度和所有子元素的寬度都已經(jīng)計(jì)算出來(lái)了,我們現(xiàn)在可以開(kāi)始計(jì)算可用空間。

        現(xiàn)在,我們只需遍歷該數(shù)組,計(jì)算子元素的寬度,將這些總和與父 div 比較,并找到「最后一個(gè)可見(jiàn)項(xiàng)目」

        4. 處理“更多”按鈕

        當(dāng)我們胸有成竹的把上述代碼運(yùn)行后,猛然發(fā)現(xiàn),我們還缺失了一個(gè)重要的步驟:如何在瀏覽器中渲染更多按鈕。我們也需要考慮它的寬度。

        同樣,我們只能在瀏覽器中渲染它時(shí)才能獲取其寬度。因此,我們必須在「首次渲染」期間明確添加按鈕:

              
              const Component = ({ items }) => {
          return (
            <div className="navigation">
              {items.map((item) => (
                <a href={item.href}>{item.name}</a>
              ))}
              {/* 在鏈接后明確添加“更多”按鈕 */}
              <button id="more">...</button>
            </div>

          );
        };

        5. 函數(shù)抽離

        如果我們將計(jì)算寬度的所有邏輯抽象成一個(gè)函數(shù),那么在我們的useEffect中會(huì)有類似這樣的東西:

              
              useEffect(() => {
          const { moreWidth, necessaryWidths, containerWidth } = getPrecalculatedWidths(
            ref.current
          );

          const itemIndex = getLastVisibleItem({
            containerWidth,
            necessaryWidths,
            moreWidth,
          });
        }, [ref]);

        getPrecalculatedWidths

              
              // 定義右側(cè)間隙的常量
        const rightGap = 10;
        // 獲取子元素的預(yù)先計(jì)算寬度信息
        const getPrecalculatedWidths = (element: HTMLElement) => {
          // 獲取容器的寬度和左側(cè)位置
          const {
            width: containerWidth,
            left: containerLeft
          } = element.getBoundingClientRect();

          // 獲取容器的所有子元素
          const children = Array.from(element.childNodes) as HTMLElement[];

          // 初始化“more”按鈕寬度和子元素寬度數(shù)組
          let moreWidth = 0;
          const necessaryWidths = children.reduce<number[]>((result, node) => {
            // 提取“more”按鈕的寬度并跳過(guò)計(jì)算
            if (node.getAttribute("id") === "more") {
              moreWidth = node.getBoundingClientRect().width;
              return result;
            }

            // 計(jì)算子元素的寬度,考慮了左側(cè)位置和右側(cè)間隙
            const rect = node.getBoundingClientRect();
            const width = rect.width + (rect.left - containerLeft) + rightGap;

            return [...result, width];
          }, []);

          // 返回預(yù)先計(jì)算的寬度信息對(duì)象
          return {
            moreWidth,
            necessaryWidths,
            containerWidth
          };
        };

        getLastVisibleItem

        其中getLastVisibleItem函數(shù)執(zhí)行所有數(shù)學(xué)計(jì)算并返回一個(gè)數(shù)字——最后一個(gè)可以適應(yīng)可用空間的鏈接的索引。

              
              // 獲取在給定容器寬度內(nèi)可見(jiàn)的最后一個(gè)子元素的索引
        const getLastVisibleItem = ({
          necessaryWidths,
          containerWidth,
          moreWidth,
        }: {
          necessaryWidths: number[],
          containerWidth: number,
          moreWidth: number,
        }) => {
          // 如果沒(méi)有子元素寬度信息,返回0
          if (!necessaryWidths?.length) return 0;

          // 如果最后一個(gè)子元素寬度小于容器寬度,說(shuō)明所有元素都能完全顯示
          if (necessaryWidths[necessaryWidths.length - 1] < containerWidth) {
            return necessaryWidths.length - 1;
          }

          // 過(guò)濾出所有寬度加上“more”按鈕寬度小于容器寬度的子元素
          const visibleItems = necessaryWidths.filter((width) => {
            return width + moreWidth < containerWidth;
          });

          // 返回可見(jiàn)子元素的最后一個(gè)的索引,如果沒(méi)有可見(jiàn)的元素,則返回0
          return visibleItems.length ? visibleItems.length - 1 : 0;
        };

        React角度來(lái)看,我們既然得到了這個(gè)數(shù)字,我們就需要觸發(fā)組件的更新,并讓它刪除不應(yīng)該展示的組件。

        我們需要在獲取該數(shù)字時(shí)將其保存在狀態(tài)中:

              
              const Component = ({ items }) => {
          // 將初始值設(shè)置為-1,以表示我們尚未運(yùn)行計(jì)算
          const [lastVisibleMenuItem, setLastVisibleMenuItem] = useState(-1);

          useEffect(() => {
            const itemIndex = getLastVisibleItem(ref.current);
            // 使用實(shí)際數(shù)字更新?tīng)顟B(tài)
            setLastVisibleMenuItem(itemIndex);
          }, [ref]);
        };

        然后,在渲染菜單時(shí),考慮根據(jù)lastVisibleMenuItem來(lái)控制子元素的內(nèi)容

              
              const Component = ({ items }) => {

          // 如果是第一次渲染且值仍然是默認(rèn)值,則渲染所有內(nèi)容
          if (lastVisibleMenuItem === -1) {
            // 在這里渲染所有項(xiàng)目,與以前相同
            return ...
          }

          // 如果最后可見(jiàn)的項(xiàng)目不是數(shù)組中的最后一個(gè),則顯示“更多”按鈕
          const isMoreVisible = lastVisibleMenuItem < items.length - 1;

          // 過(guò)濾掉那些索引大于最后可見(jiàn)的項(xiàng)目的項(xiàng)目
          const filteredItems = items.filter((item, index) => index <= lastVisibleMenuItem);

          return (
            <div className="navigation">
              {/* 僅呈現(xiàn)可見(jiàn)項(xiàng)目 */}
              {filteredItems.map(item => <a href={item.href}>{item.name}</a>)}
              {/* 有條件地呈現(xiàn)“更多” */}
              {isMoreVisible && <button id="more">...</button>}
            </div>

          )
        }

        現(xiàn)在,在state用實(shí)際數(shù)字更新后,它將觸發(fā)導(dǎo)航的重新渲染,React 將重新渲染項(xiàng)目并刪除那些不可見(jiàn)的項(xiàng)目。

        6. 監(jiān)聽(tīng) resize 事件

        為了實(shí)現(xiàn)真正的響應(yīng)式,我們還需要監(jiān)聽(tīng)resize事件并重新計(jì)算數(shù)字。

              
              // 用dimensions來(lái)存儲(chǔ) necessaryWidths和moreWidth
        const [dimensions, setDimensions] = useState<{
            necessaryWidths: number[];
            moreWidth: number;
          }>({
            necessaryWidths: [],
            moreWidth0
          });

        useEffect(() => {
          const listener = () => {
            if (!ref.current) return;
            const newIndex = getLastVisibleItem({
              containerWidth: ref.current.getBoundingClientRect().width,
              necessaryWidths: dimensions.necessaryWidths,
              moreWidth: dimensions.moreWidth,
            });

            if (newIndex !== lastVisibleMenuItem) {
              setLastVisibleMenuItem(newIndex);
            }
          };

          window.addEventListener("resize", listener);

          return () => {
            window.removeEventListener("resize", listener);
          };
        }, [lastVisibleMenuItem, dimensions, ref]);

        上面的代碼雖然不是全部的代碼,但是主要的邏輯就是實(shí)現(xiàn)在響應(yīng)式的組件,并且能夠在屏幕大小發(fā)生變化時(shí)重新計(jì)算寬度。

        但是呢,在在 CPU 計(jì)算能力下降時(shí),出產(chǎn)生內(nèi)容閃動(dòng)的情況。也就是,在某個(gè)時(shí)刻,我們先看到所有的項(xiàng)目和更多按鈕,隨后,根據(jù)可用空間的多少,會(huì)隱藏掉部分項(xiàng)目。


        3. 使用 useLayoutEffect 修復(fù)閃爍問(wèn)題

        上面出現(xiàn)閃爍的根本原因就是:我們先把所有元素都渲染出來(lái)了,然后依據(jù)計(jì)算后的剩余空間來(lái)控制哪些元素可見(jiàn)/隱藏。 也就是我們做的是一種「先渲染再刪除」的操作。在useLayoutEffect沒(méi)出現(xiàn)之前,其實(shí)大家解決這類問(wèn)題的方式都很奇葩。還是沿用第一次渲染全部元素,但是設(shè)置這些元素不可見(jiàn)(不透明度設(shè)置為 0/或者在可見(jiàn)區(qū)域之外的某個(gè)地方的某個(gè) div 中呈現(xiàn)這些元素),然后在計(jì)算后再將那些滿足條件的元素顯示出來(lái)。

        然而,在 React 16.8+,我們可以用 useLayoutEffect 替換 useEffect 鉤子。

              
              const Component = ({ items }) => {
          // 一切都完全相同,只是鉤子的名稱不同
          useLayoutEffect(() => {
            // 代碼仍然一樣
          }, [ref]);
        };

        僅需要一行代碼就可以解決上面的閃爍問(wèn)題。神不神奇。

        a623d5185bde6aab53ef40b2d3aa5ffc.webp

        雖然,useLayoutEffect能解決我們的問(wèn)題,但是根據(jù)React 官方文檔[2],它是有一定的缺陷的。

        bc187ba0dd666783a62c5fe0ce2e7e4f.webp
        • 文檔明確表示 useLayoutEffect 可能會(huì)影響性能,應(yīng)該避免使用。
        • 文檔還說(shuō)它在瀏覽器重新繪制屏幕之前觸發(fā),這意味著 useEffect 在其后觸發(fā)。

        雖然,useLayoutEffect能解決我們的問(wèn)題,但是也有一定的風(fēng)險(xiǎn)。所以,我們需要對(duì)其有一個(gè)更深的認(rèn)知,這樣才可以在遇到類似的問(wèn)題,有的放矢。

        然后,要想深入了解useLayoutEffect,就需要從瀏覽器的角度來(lái)探查原因了。

        so,讓我們講點(diǎn)瀏覽器方面的東西。


        4. 瀏覽器如何渲染頁(yè)面
        ?

        我們之前在EventLoop = TaskQueue + RenderQueueEventLoop的角度分析了,瀏覽器渲染頁(yè)面的流程。所以,我們就簡(jiǎn)單的回顧一下。

        ?

        「瀏覽器不會(huì)實(shí)時(shí)連續(xù)地更新屏幕上需要顯示的所有內(nèi)容」,而是會(huì)將所有內(nèi)容分成一系列幀,并逐幀地顯示它們。在瀏覽器中,我們可以看到這些幀,它們被稱為,或者幀緩沖,因?yàn)樗鼈兪菫g覽器用來(lái)顯示內(nèi)容的一系列幀。

        ?

        瀏覽器顯示頁(yè)面的過(guò)程像你像領(lǐng)導(dǎo)展示PPT的過(guò)程。

        ?

        你展示了一張PPT,然后等待他們理解你天馬行空的創(chuàng)意后,隨后你才可以切換到一張PPT。就這樣周而復(fù)始的執(zhí)行上面的操作。

        如果一個(gè)非常慢的瀏覽器被要求制定如何畫貓頭鷹的指令,它可能實(shí)際上會(huì)是如下的步驟:740dd336c97d15fa99084b9bea8d3d71.webp

        1. 第一步:畫了兩個(gè)圓
        2. 第二步:把剩余的所有細(xì)節(jié)都補(bǔ)充完成

        上述的過(guò)程非??臁Mǔ?,現(xiàn)代瀏覽器嘗試保持 60 FPS 的速率,即每秒 60 幀。每 16.6 毫秒左右切換一張PPT

        渲染任務(wù)

        ?

        更新這些PPT的信息被分成任務(wù)。

        ?

        任務(wù)被放入隊(duì)列中。瀏覽器從隊(duì)列中抓取一個(gè)任務(wù)并執(zhí)行它。如果有更多時(shí)間,它執(zhí)行下一個(gè)任務(wù),依此類推,直到在16.6ms 的間隙中沒(méi)有更多時(shí)間為止,然后刷新屏幕。然后繼續(xù)不停地工作,以便我們能夠進(jìn)行一些重要的事情。

        在正常的 Javascript 中,任務(wù)是我們放在腳本中并「同步執(zhí)行」的所有內(nèi)容。

              
              const app = document.getElementById("app");
        const child = document.createElement("div");
        child.innerHTML = "<h1>前端柒八九!</h1>";
        app.appendChild(child);

        child.style = "border: 10px solid red";
        child.style = "border: 20px solid green";
        child.style = "border: 30px solid black";

        如上我們通過(guò)id 獲取一個(gè)元素,將它放入 app 變量中,創(chuàng)建一個(gè) div,更新其 HTML,將該 div 附加到 app,然后三次更改 div 的邊框。「對(duì)于瀏覽器來(lái)說(shuō),整個(gè)過(guò)程將被視為一個(gè)任務(wù)」。因此,它將執(zhí)行每一行,然后繪制最終結(jié)果:帶有黑色邊框的 div。

        我們「無(wú)法在屏幕上看到這個(gè)紅綠黑的過(guò)渡」。

        如果任務(wù)花費(fèi)的時(shí)間超過(guò) 16.6ms 會(huì)發(fā)生什么呢?。瀏覽器不能停止它或拆分它。它「將繼續(xù)進(jìn)行,直到完成,然后繪制最終結(jié)果」。如果我在這些邊框更新之間添加 1 秒的同步延遲

              
              const waitSync = (ms) => {
          let start = Date.now(),
            now = start;
          while (now - start < ms) {
            now = Date.now();
          }
        };

        child.style = "border: 10px solid red";
        waitSync(1000);
        child.style = "border: 20px solid green";
        waitSync(1000);
        child.style = "border: 30px solid black";
        waitSync(1000);

        我們?nèi)匀粺o(wú)法看到“中間”結(jié)果。我們只會(huì)盯著空白屏幕直到瀏覽器解決它,并在最后看到黑色邊框。這就是我們所說(shuō)的阻塞渲染代碼。

        盡管 React 也是 Javascript,但是不是作為一個(gè)單一的任務(wù)執(zhí)行的。我們可以通過(guò)各種異步方式(回調(diào)、事件處理程序、promises 等)「將整個(gè)應(yīng)用程序渲染為更小的任務(wù)」

        如果我只是用 setTimeout 包裝那些樣式調(diào)整,即使是 0 延遲:

              
              setTimeout(() => {
          child.style = "border: 10px solid red";
          wait(1000);
          setTimeout(() => {
            child.style = "border: 20px solid green";
            wait(1000);
            setTimeout(() => {
              child.style = "border: 30px solid black";
              wait(1000);
            }, 0);
          }, 0);
        }, 0);

        這里處理方式和我們之前處理堆棧溢出的方式是一樣的。

        然后,每個(gè)定時(shí)器都將被視為一個(gè)新的任務(wù)。因此,瀏覽器將能夠在完成一個(gè)任務(wù)之后并在開(kāi)始下一個(gè)任務(wù)之前重新繪制屏幕。我們將能夠看到從紅到綠再到黑的緩慢的過(guò)渡,而不是在白屏上停留三秒鐘。

        ?

        這就是 React 為我們所做的事情。實(shí)質(zhì)上,它是一個(gè)非常復(fù)雜且高效的引擎,將由數(shù)百個(gè) npm 依賴項(xiàng)與我們自己的代碼組合而成的塊分解成瀏覽器能夠在 16.6ms 內(nèi)處理的最小塊。

        ?

        5. useEffect vs useLayoutEffect

        回到上面話題,為什么我們用了useLayoutEffect就解決了頁(yè)面閃爍的問(wèn)題。

        ?

        useLayoutEffectReact 在組件更新期間「同步運(yùn)行的內(nèi)容」。

        ?
              
              const Component = () => {
          useLayoutEffect(() => {
            // 做一些事情
          });

          return ...;
        };

        我們?cè)诮M件內(nèi)部渲染的任何內(nèi)容都將與 useLayoutEffect 被統(tǒng)籌為同一任務(wù)。即使在 useLayoutEffect 內(nèi)部更新state(我們通常認(rèn)為這是一個(gè)異步任務(wù)),React 仍然會(huì)確保「整個(gè)流程以同步方式運(yùn)行」。

        如果我們回到一開(kāi)始實(shí)現(xiàn)的導(dǎo)航示例。從瀏覽器的角度來(lái)看,它只是一個(gè)任務(wù)

        a0fd7b5bb35bd0b77b63112f245535f5.webp

        這種情況與我們無(wú)法看到的紅綠黑邊框過(guò)渡的情況完全相同!

        另一方面,使用 useEffect 的流程將分為兩個(gè)任務(wù):

        2ff9f72b81132a6eb6a135cc90c47b6f.webp

        第一個(gè)任務(wù)渲染了帶有所有按鈕的初始導(dǎo)航。而第二個(gè)任務(wù)刪除我們不需要的那些子元素。在「兩者之間重新繪制屏幕」!與setTimeout內(nèi)的邊框情況完全相同。

        所以回答我們一開(kāi)始的問(wèn)題。使用 useLayoutEffect它會(huì)影響性能!我們最不希望的是我們整個(gè) React 應(yīng)用程序變成一個(gè)巨大的同步任務(wù)

        ?

        只有在需要根據(jù)元素的實(shí)際大小調(diào)整 UI 而導(dǎo)致的視覺(jué)閃爍時(shí)使用 useLayoutEffect。對(duì)于其他所有情況,useEffect 是更好的選擇。

        ?

        對(duì)于useEffect有一點(diǎn)我們需要額外說(shuō)明一下。

        ?

        大家都認(rèn)為 useEffect在瀏覽器渲染后觸發(fā),其實(shí)不完全對(duì)。

        ?

        useEffect 有時(shí)在渲染前執(zhí)行

        在正常的流程中,React 更新過(guò)程如下:

        1. React工作:渲染虛擬DOM,安排effect,更新真實(shí)DOM
        2. 調(diào)用 useLayoutEffect
        3. React 釋放控制,瀏覽器繪制新的DOM
        4. 調(diào)用 useEffect

        React文檔并沒(méi)有明確說(shuō)明 useEffect 何時(shí)確切地執(zhí)行,它發(fā)生在「布局和繪制之后,通過(guò)延遲事件進(jìn)行」。

        然而,在文檔中有一個(gè)更有趣的段落:

        ?

        盡管 useEffect 被延遲到瀏覽器繪制之后,但它保證在「任何新的渲染之前」執(zhí)行。React總是會(huì)在「開(kāi)始新的更新之前刷新前一個(gè)渲染」effect。

        ?

        如果 useLayoutEffect 觸發(fā)state更新時(shí),那么effect必須在那次更新之前被刷新,即在繪制之前。下面是一個(gè)時(shí)間軸:

        efc8896d6f5de063fe3ceebf63cfd347.webp
        1. React 更新 1:渲染虛擬DOM,安排effect,更新DOM
        2. 調(diào)用 useLayoutEffect
        3. 更新state,安排重新渲染(re-render)
        4. 調(diào)用 useEffect
        5. React 更新 2
        6. 調(diào)用 useLayoutEffect 從更新 2
        7. React 釋放控制,瀏覽器繪制新的DOM
        8. 調(diào)用 useEffect 從更新 2

        在瀏覽者中就會(huì)出現(xiàn)如下的瀑布流。2c822bb22dc882d3c567a66e813509ce.webp

        上面的案例說(shuō)明了,useLayoutEffect可以在繪制之前強(qiáng)制提前刷新effect。而像

        • ref <div ref={HERE}>
        • requestAnimationFrame
        • useLayoutEffect 調(diào)度的微任務(wù)

        也會(huì)觸發(fā)相同的行為。

        如果,我們不想在useLayoutEffect強(qiáng)制刷新useEffect。我們可以跳過(guò)狀態(tài)更新。

        使用ref直接對(duì)DOM進(jìn)行修改。這樣,React不會(huì)安排更新,也不需要急切地刷新effect。

              
              const clearRef = useRef();
        const measure = () => {
          // 不用擔(dān)心 react,我會(huì)處理的:
          clearRef.current.display = el.current.offsetWidth > 200 ? null : 'none';
        };
        useLayoutEffect(() => measure(), []);
        useEffect(() => {
          window.addEventListener("resize", measure);
          return () => window.removeEventListener("resize", measure);
        }, []);
        return (
          <label>
            <input {...propsref={el} />
            <button ref={clearRef} onClick={onClear}>clear</button>
          </label>

        );

        6. 在 Next.js 和其他 SSR 框架中使用 useLayoutEffect

        當(dāng)我們將使用useLayoutEffect處理過(guò)的自適應(yīng)導(dǎo)航組件寫入到任何一個(gè)SSR框架時(shí),你會(huì)發(fā)現(xiàn)它還是會(huì)產(chǎn)生閃爍現(xiàn)象。

        當(dāng)我們啟用了 SSR 時(shí),意味著在后端的某個(gè)地方調(diào)用類似React.renderToString(<App />)的東西。然后,React 遍歷應(yīng)用中的所有組件,“渲染”它們(即調(diào)用它們的函數(shù),它們畢竟只是函數(shù)),然后生成這些組件表示的 HTML。

        59e32d40134d9d2950e6e6938ebd68ae.webp

        然后,將此 HTML 注入要發(fā)送到瀏覽器的頁(yè)面中,「一切都在服務(wù)器上生成」。之后,瀏覽器下載頁(yè)面,向我們顯示頁(yè)面,下載所有腳本(包括 React),隨后運(yùn)行它們,React 通過(guò)預(yù)生成的 HTML,為其注入一些互動(dòng)效果,我們的頁(yè)面就會(huì)變的有交互性了。

        問(wèn)題在于:在我們生成初始 HTML 時(shí),還沒(méi)有瀏覽器。因此,任何涉及計(jì)算元素實(shí)際大小的操作(就像我們?cè)?useLayoutEffect 中做的那樣)在服務(wù)器上將不起作用:只有字符串,而沒(méi)有具有尺寸的元素。而且由于 useLayoutEffect 的整個(gè)目的是獲得對(duì)元素大小的訪問(wèn)權(quán),因此在服務(wù)器上運(yùn)行它沒(méi)有太多意義。

        因此,我們?cè)跒g覽器顯示我們的頁(yè)面之前在“第一次通過(guò)”階段渲染的內(nèi)容就是在我們組件中渲染的內(nèi)容:所有按鈕的一行,包括“更多”按鈕。在瀏覽器有機(jī)會(huì)執(zhí)行所有內(nèi)容并使 React 變得活躍之后,它最終可以運(yùn)行 useLayoutEffect,最終按鈕才會(huì)隱藏。但視覺(jué)故障依然存在。

        如何解決這個(gè)問(wèn)題涉及用戶體驗(yàn)問(wèn)題,完全取決于我們想“默認(rèn)”向用戶展示什么。我們可以向他們顯示一些“加載”狀態(tài)而不是菜單?;蛘咧伙@示一兩個(gè)最重要的菜單項(xiàng)?;蛘呱踔镣耆[藏項(xiàng)目,并僅在客戶端上渲染它們。這取決于你。

        一種方法是引入一些shouldRender狀態(tài)變量,并在 useEffect 中將其變?yōu)?code style="background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">true:

              
              const Component = () => {
          const [shouldRender, setShouldRender] = useState(false);

          useEffect(() => {
            setShouldRender(true);
          }, []);

          if (!shouldRender) return <SomeNavigationSubstitute />;

          return <Navigation />;
        };

        useEffect 只會(huì)在客戶端運(yùn)行,因此初始 SSR 通過(guò)將向我們顯示替代組件。然后,客戶端代碼將介入,useEffect 將運(yùn)行,狀態(tài)將更改,React 將其替換為正常的響應(yīng)式導(dǎo)航。


        后記

        「分享是一種態(tài)度」。

        「全文完,既然看到這里了,如果覺(jué)得不錯(cuò),隨手點(diǎn)個(gè)贊和“在看”吧?!?/strong>

        5111bb315abb5c681f23402687bb2087.webp

        Reference

        [1]

        觸發(fā)強(qiáng)制布局的操作: https://gist.github.com/paulirish/5d52fb081b3570c81e3a

        [2]

        React 官方文檔: https://react.dev/reference/react/useLayoutEffect


        瀏覽 146
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

          <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            中国一级黄色毛片 | 成人伊人综合 | 国产精品熟女视频 | 美女免费黄片 | 国产高清毛片视频 | 91人人干 | 沙奈朵狂揉下部羞羞动漫 | 日批毛片 | 爱情岛论坛成人 | 美女脱裤子让人桶爽免费网站 |