魯斯前端布魯斯前端

文章中英模式

布魯斯前端React面試題目 - useEffect 與 useLayoutEffect的使用選擇

深入比較useEffect與useLayoutEffect的執行時機、適用場景與效能影響。了解兩者在React渲染流程中的區別,正確選擇何時使用useLayoutEffect來避免閃爍問題,以及如何在SSR環境中正確處理。

影片縮圖

懶得看文章?那就來看影片吧

兩者執行時機的根本區別

useEffectuseLayoutEffect的API完全相同,但它們在React渲染流程中的執行時機不同:

Hook執行時機同步/非同步畫面更新時機
useEffect渲染後,瀏覽器繪製後非同步使用者先看到更新,然後執行effect
useLayoutEffect渲染後,瀏覽器繪製前同步執行完effect後,使用者才看到更新

渲染流程對比圖:

useEffect的流程:

  1. 1. React更新虛擬DOM並計算差異
  2. 2. React更新實際DOM
  3. 3. 瀏覽器繪製畫面(使用者看到更新)
  4. 4. 然後執行useEffect回調

useLayoutEffect的流程:

  1. 1. React更新虛擬DOM並計算差異
  2. 2. React更新實際DOM
  3. 3. 執行useLayoutEffect回調
  4. 4. 瀏覽器繪製畫面(使用者看到更新)
// 基本語法相同,但執行時機不同
import { useEffect, useLayoutEffect } from 'react';

function ExampleComponent() {
  const [state, setState] = useState(initialState);
  
  // 在畫面繪製後執行(非阻塞)
  useEffect(() => {
    // 這裡的代碼在畫面更新後執行
    // 即使執行耗時操作,使用者也已經看到了UI
  }, [dependencies]);
  
  // 在畫面繪製前執行(阻塞)
  useLayoutEffect(() => {
    // 這裡的代碼在DOM更新後但畫面繪製前執行
    // 在這裡的操作會阻塞畫面更新,直到執行完成
  }, [dependencies]);
  
  return <div>...</div>;
}

兩者的適用場景對比

簡單來說,useEffectuseLayoutEffect的選擇取決於你需要副作用何時執行:

Hook適用場景實際例子
useEffect
推薦優先使用
  • 1. 資料獲取
  • 2. 事件監聽
  • 3. 訂閱設置
  • 4. 計時器操作
  • 5. 日誌記錄
  • 1. API請求獲取用戶資料
  • 2. 監聽視窗大小變化
  • 3. 設置WebSocket連接
  • 4. 定時更新在線狀態
useLayoutEffect
特定場景使用
  • 1. DOM測量
  • 2. 視覺動畫初始化
  • 3. 防止閃爍
  • 4. 元素定位
  • 1. 測量元素高度後調整佈局
  • 2. 工具提示精確定位
  • 3. 滾動位置調整
  • 4. 拖拽元件初始化

記住: 除非你需要在畫面繪製前進行DOM操作(避免閃爍或需要精確測量),否則應該使用useEffectuseLayoutEffect會阻塞渲染,可能影響性能。

對性能的影響

選擇合適的hook會對應用的性能產生明顯影響:

useEffect的性能特性

  • 1. 非阻塞渲染 - 畫面更新不會等待effect完成
  • 2. 允許瀏覽器先繪製UI再執行耗時操作
  • 3. 對於大多數副作用來說更高效
  • 4. 可能導致視覺閃爍(如果effect改變了可見元素)

useLayoutEffect的性能特性

  • 1. 阻塞渲染 - 會延遲畫面更新直到effect完成
  • 2. 在effect中的耗時操作會導致明顯的UI延遲
  • 3. 避免了閃爍問題
  • 4. 對於需要同步DOM更新的操作更合適
// 性能影響案例:閃爍問題

// ❌ 使用useEffect可能導致閃爍
function FlickeringExample() {
  const [width, setWidth] = useState(0);
  const divRef = useRef();
  
  useEffect(() => {
    // 問題:先渲染初始狀態,用戶看到這一幀
    // 然後才測量並更新,導致內容跳動
    setWidth(divRef.current?.getBoundingClientRect().width || 0);
  }, []);

  return <div ref={divRef} style={{ width: `${width}px` }}>內容可能閃爍</div>;
}

// ✅ 使用useLayoutEffect避免閃爍
function SmoothExample() {
  const [width, setWidth] = useState(0);
  const divRef = useRef();
  
  useLayoutEffect(() => {
    // 優點:在畫面繪製前完成測量和更新
    // 用戶只會看到最終結果,不會看到中間過程
    setWidth(divRef.current?.getBoundingClientRect().width || 0);
  }, []);

  return <div ref={divRef} style={{ width: `${width}px` }}>內容不會閃爍</div>;
}

實用例子:何時選擇每一種

使用 useLayoutEffect 的典型例子

// 例子1:彈出提示定位 - 在畫面繪製前計算位置
function Tooltip({ text, targetRef }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef(null);
  
  useLayoutEffect(() => {
    // 獲取目標元素和提示框的尺寸
    // 計算提示框位置(置於目標元素上方中央)
    // 立即更新位置狀態,確保渲染前完成
    if (targetRef.current && tooltipRef.current) {
      const targetRect = targetRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      setPosition({
        top: targetRect.top - tooltipRect.height - 10,
        left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2
      });
    }
  }, [targetRef]);
  
  return <div ref={tooltipRef} style={{position: 'fixed', top: `${position.top}px`, left: `${position.left}px`}}>{text}</div>;
}

// 例子2:滾動到特定元素 - 避免閃爍
function ScrollToElement({ elementId }) {
  useLayoutEffect(() => {
    // 找到目標元素並立即滾動到該位置
    // 在畫面繪製前執行,用戶不會看到中間過程
    const element = document.getElementById(elementId);
    if (element) element.scrollIntoView({ behavior: 'smooth' });
  }, [elementId]);
  
  return null;
}

使用 useEffect 的典型例子

// 例子1:資料讀取
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // 非阻塞操作,使用者可以先看到載入狀態
    setLoading(true);
    
    fetchUser(userId)
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error(error);
        setLoading(false);
      });
  }, [userId]);
  
  if (loading) return <div>載入中...</div>;
  if (!user) return <div>找不到使用者</div>;
  
  return <div>{user.name}</div>;
}

SSR (伺服器端渲染) 的特殊考量

在服務器端渲染(SSR)環境中,useLayoutEffect會顯示警告並且不會執行,因為服務器上沒有「佈局階段」。這是因為:

  • 1. 服務器沒有瀏覽器環境,無法進行DOM測量或更新
  • 2. SSR過程中不存在「瀏覽器繪製」的概念
  • 3. 服務器上的React只渲染出HTML字符串,不涉及實際DOM操作

處理SSR環境中的useLayoutEffect

// 方法1:條件性使用不同的hook
import { useEffect, useLayoutEffect } from 'react';

// 根據環境選擇適當的hook
const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

function MyComponent() {
  useIsomorphicLayoutEffect(() => {
    // 在瀏覽器中,這會表現得像useLayoutEffect
    // 在服務器上,這會表現得像useEffect(實際上不執行)
    // ...
  }, []);
  
  return <div>...</div>;
}

// 方法2:將DOM操作延遲到客戶端
function SSRSafeComponent() {
  const [isMounted, setIsMounted] = useState(false);
  
  // 在客戶端首次渲染時設置為已掛載
  useEffect(() => {
    setIsMounted(true);
  }, []);
  
  // 只在客戶端渲染時才執行DOM操作
  useLayoutEffect(() => {
    // 這個effect只會在客戶端執行
    if (isMounted) {
      // 安全進行DOM測量和操作...
    }
  }, [isMounted]);
  
  return <div>...</div>;
}

🔥 常見面試題目

(一) useEffect和useLayoutEffect的主要區別是什麼?

解答: 兩者的主要區別在於執行時機:

useEffect

• 非同步執行

• 畫面更新後執行

• 不阻塞渲染

useLayoutEffect

• 同步執行

• 畫面更新前執行

• 阻塞渲染

兩者API完全相同,只是執行時機不同,適用於不同場景。

(二) 為什麼大多數情況下應該優先使用useEffect?

解答: 優先使用useEffect的原因:

  • 1. 性能更好:不阻塞畫面更新
  • 2. 適合大多數場景:資料獲取、訂閱設置等不需要阻塞UI
  • 3. SSR友好:服務器端渲染環境中表現更一致
  • 4. 避免卡頓:耗時操作不會延遲UI顯示

(三) 什麼情況下應該使用useLayoutEffect?

解答: 適合useLayoutEffect的場景:

防止閃爍

元素位置或樣式需要在用戶看到前調整

DOM測量與定位

工具提示、彈出層需要精確定位

動畫初始化

設置正確初始狀態避免跳動

滾動位置控制

在頁面顯示前調整滾動位置

// 避免閃爍的簡單例子
function Tooltip({ text, targetRef }) {
  const tooltipRef = useRef(null);
  
  useLayoutEffect(() => {
    // 測量目標元素位置
    const targetRect = targetRef.current.getBoundingClientRect();
    // 在畫面更新前定位提示框
    tooltipRef.current.style.top = targetRect.bottom + 'px';
    tooltipRef.current.style.left = targetRect.left + 'px';
  }, []);
  
  return <div ref={tooltipRef}>{text}</div>;
}

(四) 如何在SSR環境中安全使用useLayoutEffect?

解答: 服務器沒有DOM,無法執行布局效果。解決方案:

  1. 1. 創建同構hook,根據環境選擇不同hook
  2. 2. 使用客戶端檢測,確保只在瀏覽器執行
// 同構hook方案
const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' 
    ? useLayoutEffect 
    : useEffect;

// 客戶端檢測方案
function SafeComponent() {
  const [isBrowser, setIsBrowser] = useState(false);
  
  useEffect(() => {
    setIsBrowser(true);
  }, []);
  
  useLayoutEffect(() => {
    if (isBrowser) {
      // 安全進行DOM操作
    }
  }, [isBrowser]);
  
  return <div>...</div>;
}

最佳實踐:將需要布局測量的代碼分離到純客戶端組件中。