記錄---從零開始編寫 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
},
}
}
這里的核心思路是:當組件訪問 width 或 height 時,我們記錄下這個依賴關系,然后在窗口變化時只檢查被使用的屬性。
第四步:使用 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 的過程中,我們遵循了以下設計思路:
- 從簡單開始:先實現基本功能,再逐步優化
- 解決 SSR 問題:確保服務端和客戶端首次渲染一致,避免 hydration mismatch
- 性能優化:通過依賴追蹤減少不必要的重新渲染
- 現代化 API:使用 React 18 的新特性提升并發安全性
- 類型安全:添加 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>
)
}
性能優勢
這個實現提供了幾個性能優勢:
- 選擇性更新:只有訪問的屬性變化時才重新渲染
- 事件去重:多個組件共享同一個事件監聽器
- 內存效率:盡可能重用對象而不是創建新對象
- 并發安全:與 React 的并發特性完美配合
通過這樣的步驟,我們從最簡單的實現開始,逐步解決了各種問題,最終得到了一個高性能、類型安全、SSR 兼容的 useWindowSize Hook。
本文轉載于:https://juejin.cn/post/7530635412848836646
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。


浙公網安備 33010602011771號