死亡汉堡免安装绿色中文版
1G · 2025-09-27
点赞 + 收藏 === 学会???
在 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}
这个版本能工作,但存在几个问题:
服务端渲染时没有 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
时,我们记录下这个依赖关系,然后在窗口变化时只检查被使用的属性。
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 }, }}
最后,让我们添加完整的类型定义:
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 的过程中,我们遵循了以下设计思路:
这个实现的精髓在于依赖追踪系统。通过使用 getter 函数,我们可以检测组件实际使用了哪些属性,并且只在这些特定属性发生变化时才触发更新。
关键是确保服务端渲染和客户端首次渲染返回相同的初始值。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> )}
这个实现提供了几个性能优势:
通过这样的步骤,我们从最简单的实现开始,逐步解决了各种问题,最终得到了一个高性能、类型安全、SSR 兼容的 useWindowSize
Hook。