动态

详情 返回 返回

記錄---從零開始編寫 useWindowSize Hook - 动态 详情

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

在 React 開發中,我們經常需要根據窗口大小來調整組件的行為。今天我們將從最簡單的實現開始,逐步優化,最終構建出一個高性能的 useWindowSize Hook。

第一步:最簡單的實現

讓我們從最基礎的版本開始:

import { useState, useEffect } from 'react'

function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  })

  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      })
    }

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return windowSize
}

這個版本能工作,但存在幾個問題:

  • 每次窗口變化都會創建新對象,導致不必要的重新渲染
  • 沒有考慮服務端渲染
  • 性能不夠優化

第二步:解決 SSR 問題

服務端渲染時沒有 window 對象,而且需要避免 hydration mismatch 錯誤:

import { useState, useEffect } from 'react'

function useWindowSize() {
  // 關鍵:服務端和客户端首次渲染都返回相同的初始值
  const [windowSize, setWindowSize] = useState({
    width: 0,
    height: 0,
  })

  useEffect(() => {
    function updateSize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      })
    }

    // 客户端首次執行時立即獲取真實尺寸
    updateSize()

    // 然後監聽後續變化
    window.addEventListener('resize', updateSize)
    return () => window.removeEventListener('resize', updateSize)
  }, [])

  return windowSize
}

這裏的關鍵是確保服務端和客户端首次渲染時返回相同的值,避免 hydration mismatch。

第三步:性能優化 - 減少不必要的更新

現在我們思考一個問題:如果組件只使用了 width,那麼 height 變化時是否需要重新渲染?答案是不需要。

讓我們引入依賴追蹤的概念:

import { useRef, useState, useEffect } from 'react'

function useWindowSize() {
  const stateDependencies = useRef<{ width?: boolean; height?: boolean }>({})
  
  const [windowSize, setWindowSize] = useState({
    width: 0,
    height: 0,
  })

  const previousSize = useRef(windowSize)

  useEffect(() => {
    function handleResize() {
      const newSize = {
        width: window.innerWidth,
        height: window.innerHeight,
      }

      // 只檢查組件實際使用的屬性
      let shouldUpdate = false
      for (const key in stateDependencies.current) {
        if (newSize[key as keyof typeof newSize] !== previousSize.current[key as keyof typeof newSize]) {
          shouldUpdate = true
          break
        }
      }

      if (shouldUpdate) {
        previousSize.current = newSize
        setWindowSize(newSize)
      }
    }

    // 立即獲取初始尺寸
    handleResize()

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  // 使用 getter 來追蹤依賴
  return {
    get width() {
      stateDependencies.current.width = true
      return windowSize.width
    },
    get height() {
      stateDependencies.current.height = true
      return windowSize.height
    },
  }
}
 

這裏的核心思路是:當組件訪問 widthheight 時,我們記錄下這個依賴關係,然後在窗口變化時只檢查被使用的屬性。

第四步:使用 useSyncExternalStore 提升併發安全性

React 18 引入了 useSyncExternalStore,專門用於同步外部狀態,讓我們重構代碼:

import { useRef } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'

// 訂閲函數
function subscribe(callback: () => void) {
  window.addEventListener('resize', callback)
  return () => {
    window.removeEventListener('resize', callback)
  }
}

function useWindowSize() {
  const stateDependencies = useRef<{ width?: boolean; height?: boolean }>({}).current
  const previous = useRef({ width: 0, height: 0 })

  // 比較函數:只比較被使用的屬性
  const isEqual = (prev: any, current: any) => {
    for (const key in stateDependencies) {
      if (current[key] !== prev[key]) {
        return false
      }
    }
    return true
  }

  const cached = useSyncExternalStore(
    subscribe, // 訂閲函數
    () => {
      // 獲取當前狀態
      const data = {
        width: window.innerWidth,
        height: window.innerHeight,
      }
      
      // 如果有變化,更新緩存
      if (!isEqual(previous.current, data)) {
        previous.current = data
        return data
      }
      return previous.current
    },
    () => {
      // SSR 回退值 - 避免 hydration mismatch
      return { width: 0, height: 0 }
    },
  )

  return {
    get width() {
      stateDependencies.width = true
      return cached.width
    },
    get height() {
      stateDependencies.height = true
      return cached.height
    },
  }
}

第五步:添加 TypeScript 類型支持

最後,讓我們添加完整的類型定義:

import { useRef } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'

interface WindowSize {
  width: number
  height: number
}

interface StateDependencies {
  width?: boolean
  height?: boolean
}

interface UseWindowSize {
  (): {
    readonly width: number
    readonly height: number
  }
}

function subscribe(callback: () => void) {
  window.addEventListener('resize', callback)
  return () => {
    window.removeEventListener('resize', callback)
  }
}

export const useWindowSize: UseWindowSize = () => {
  const stateDependencies = useRef<StateDependencies>({}).current
  const previous = useRef<WindowSize>({
    width: 0,
    height: 0,
  })

  const isEqual = (prev: WindowSize, current: WindowSize) => {
    for (const _ in stateDependencies) {
      const t = _ as keyof StateDependencies
      if (current[t] !== prev[t]) {
        return false
      }
    }
    return true
  }

  const cached = useSyncExternalStore(
    subscribe,
    () => {
      const data = {
        width: window.innerWidth,
        height: window.innerHeight,
      }
      if (!isEqual(previous.current, data)) {
        previous.current = data
        return data
      }
      return previous.current
    },
    () => {
      // SSR 安全的初始值
      return { width: 0, height: 0 }
    },
  )

  return {
    get width() {
      stateDependencies.width = true
      return cached.width
    },
    get height() {
      stateDependencies.height = true
      return cached.height
    },
  }
}

設計思路總結

在構建這個 Hook 的過程中,我們遵循了以下設計思路:

  1. 從簡單開始:先實現基本功能,再逐步優化
  2. 解決 SSR 問題:確保服務端和客户端首次渲染一致,避免 hydration mismatch
  3. 性能優化:通過依賴追蹤減少不必要的重新渲染
  4. 現代化 API:使用 React 18 的新特性提升併發安全性
  5. 類型安全:添加 TypeScript 支持提供更好的開發體驗

關鍵概念解釋

依賴追蹤系統

這個實現的精髓在於依賴追蹤系統。通過使用 getter 函數,我們可以檢測組件實際使用了哪些屬性,並且只在這些特定屬性發生變化時才觸發更新。

SSR 兼容性

關鍵是確保服務端渲染和客户端首次渲染返回相同的初始值。useSyncExternalStore 的第三個參數專門用於提供 SSR 安全的初始值。

智能比較策略

我們維護一個緩存,只在必要時更新,顯著減少了內存分配和渲染週期。

使用示例

function MyComponent() {
  const { width, height } = useWindowSize()
  
  // 處理初始狀態(SSR 或首次加載)
  if (width === 0 && height === 0) {
    return <div>加載中...</div>
  }
  
  return (
    <div>
      <p>寬度: {width}px</p>
      <p>高度: {height}px</p>
    </div>
  )
}

// 只使用寬度的組件不會因為高度變化而重新渲染
function WidthOnlyComponent() {
  const { width } = useWindowSize()
  
  if (width === 0) {
    return <div>加載中...</div>
  }
  
  return <div>寬度: {width}px</div>
}

// 響應式佈局
function ResponsiveLayout() {
  const { width } = useWindowSize()
  
  if (width === 0) {
    return <div>加載中...</div>
  }
  
  return (
    <div>
      {width < 768 ? <MobileLayout /> : <DesktopLayout />}
    </div>
  )
}

性能優勢

這個實現提供了幾個性能優勢:

  1. 選擇性更新:只有訪問的屬性變化時才重新渲染
  2. 事件去重:多個組件共享同一個事件監聽器
  3. 內存效率:儘可能重用對象而不是創建新對象
  4. 併發安全:與 React 的併發特性完美配合

通過這樣的步驟,我們從最簡單的實現開始,逐步解決了各種問題,最終得到了一個高性能、類型安全、SSR 兼容的 useWindowSize Hook。

本文轉載於:https://juejin.cn/post/7530635412848836646

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。


user avatar chengxuyuanlaoliu2024 头像 happy2332333 头像 yishenjiroudekaixinguo 头像 dalidexiaoxiami 头像 zengh 头像 russell221 头像 doupifaner 头像 codepencil 头像 jsliang 头像 _6085362b65292 头像 jiangpengfei_5ecce944a3d8a 头像
点赞 11 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.