记录---从零开始编写 useWindowSize Hook
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<p>在 React 开发中,我们经常需要根据窗口大小来调整组件的行为。今天我们将从最简单的实现开始,逐步优化,最终构建出一个高性能的 <code>useWindowSize</code> Hook。</p>
<h2 data-id="heading-0">第一步:最简单的实现</h2>
<p>让我们从最基础的版本开始:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { useState, useEffect } from 'react'
function useWindowSize() {
const = 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
}</pre>
</div>
<p>这个版本能工作,但存在几个问题:</p>
<ul>
<li>每次窗口变化都会创建新对象,导致不必要的重新渲染</li>
<li>没有考虑服务端渲染</li>
<li>性能不够优化</li>
</ul>
<h2 data-id="heading-1">第二步:解决 SSR 问题</h2>
<p>服务端渲染时没有 <code>window</code> 对象,而且需要避免 hydration mismatch 错误:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { useState, useEffect } from 'react'
function useWindowSize() {
// 关键:服务端和客户端首次渲染都返回相同的初始值
const = 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
}</pre>
</div>
<div>
<div>
<p>这里的关键是确保服务端和客户端首次渲染时返回相同的值,避免 hydration mismatch。</p>
<h2 data-id="heading-2">第三步:性能优化 - 减少不必要的更新</h2>
<p>现在我们思考一个问题:如果组件只使用了 <code>width</code>,那么 <code>height</code> 变化时是否需要重新渲染?答案是不需要。</p>
<p>让我们引入依赖追踪的概念:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { useRef, useState, useEffect } from 'react'
function useWindowSize() {
const stateDependencies = useRef<{ width?: boolean; height?: boolean }>({})
const = 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 !== previousSize.current) {
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
},
}
}</pre>
</div>
<div> </div>
</div>
<div>
<div>
<p>这里的核心思路是:当组件访问 <code>width</code> 或 <code>height</code> 时,我们记录下这个依赖关系,然后在窗口变化时只检查被使用的属性。</p>
<h2 data-id="heading-3">第四步:使用 useSyncExternalStore 提升并发安全性</h2>
<p>React 18 引入了 <code>useSyncExternalStore</code>,专门用于同步外部状态,让我们重构代码:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">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 !== prev) {
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
},
}
}</pre>
</div>
</div>
<h2 data-id="heading-4">第五步:添加 TypeScript 类型支持</h2>
<p>最后,让我们添加完整的类型定义:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">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 !== prev) {
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
},
}
}</pre>
</div>
<div>
<div>
<h2 data-id="heading-5">设计思路总结</h2>
<p>在构建这个 Hook 的过程中,我们遵循了以下设计思路:</p>
<ol>
<li><strong>从简单开始</strong>:先实现基本功能,再逐步优化</li>
<li><strong>解决 SSR 问题</strong>:确保服务端和客户端首次渲染一致,避免 hydration mismatch</li>
<li><strong>性能优化</strong>:通过依赖追踪减少不必要的重新渲染</li>
<li><strong>现代化 API</strong>:使用 React 18 的新特性提升并发安全性</li>
<li><strong>类型安全</strong>:添加 TypeScript 支持提供更好的开发体验</li>
</ol>
<h2 data-id="heading-6">关键概念解释</h2>
<h3 data-id="heading-7">依赖追踪系统</h3>
<p>这个实现的精髓在于依赖追踪系统。通过使用 getter 函数,我们可以检测组件实际使用了哪些属性,并且只在这些特定属性发生变化时才触发更新。</p>
<h3 data-id="heading-8">SSR 兼容性</h3>
<p>关键是确保服务端渲染和客户端首次渲染返回相同的初始值。<code>useSyncExternalStore</code> 的第三个参数专门用于提供 SSR 安全的初始值。</p>
<h3 data-id="heading-9">智能比较策略</h3>
<p>我们维护一个缓存,只在必要时更新,显著减少了内存分配和渲染周期。</p>
<h2 data-id="heading-10">使用示例</h2>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">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>
)
}</pre>
</div>
<div>
<div>
<h2 data-id="heading-11">性能优势</h2>
<p>这个实现提供了几个性能优势:</p>
<ol>
<li><strong>选择性更新</strong>:只有访问的属性变化时才重新渲染</li>
<li><strong>事件去重</strong>:多个组件共享同一个事件监听器</li>
<li><strong>内存效率</strong>:尽可能重用对象而不是创建新对象</li>
<li><strong>并发安全</strong>:与 React 的并发特性完美配合</li>
</ol>
<p>通过这样的步骤,我们从最简单的实现开始,逐步解决了各种问题,最终得到了一个高性能、类型安全、SSR 兼容的 <code>useWindowSize</code> Hook。</p>
</div>
<div>
<h2>本文转载于:https://juejin.cn/post/7530635412848836646</h2>
</div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"><br></em></p>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19009258
頁:
[1]