当前位置: 首页 > news >正文

记录---从零开始编写 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 = falsefor (const key in stateDependencies.current) {if (newSize[key as keyof typeof newSize] !== previousSize.current[key as keyof typeof newSize]) {shouldUpdate = truebreak}}if (shouldUpdate) {previousSize.current = newSizesetWindowSize(newSize)}}// 立即获取初始尺寸handleResize()window.addEventListener('resize', handleResize)return () => window.removeEventListener('resize', handleResize)}, [])// 使用 getter 来追踪依赖return {get width() {stateDependencies.current.width = truereturn windowSize.width},get height() {stateDependencies.current.height = truereturn 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 }>({}).currentconst 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 = datareturn data}return previous.current},() => {// SSR 回退值 - 避免 hydration mismatchreturn { width: 0, height: 0 }},)return {get width() {stateDependencies.width = truereturn cached.width},get height() {stateDependencies.height = truereturn cached.height},}
}

第五步:添加 TypeScript 类型支持

最后,让我们添加完整的类型定义:

import { useRef } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'interface WindowSize {width: numberheight: number
}interface StateDependencies {width?: booleanheight?: boolean
}interface UseWindowSize {(): {readonly width: numberreadonly height: number}
}function subscribe(callback: () => void) {window.addEventListener('resize', callback)return () => {window.removeEventListener('resize', callback)}
}export const useWindowSize: UseWindowSize = () => {const stateDependencies = useRef<StateDependencies>({}).currentconst previous = useRef<WindowSize>({width: 0,height: 0,})const isEqual = (prev: WindowSize, current: WindowSize) => {for (const _ in stateDependencies) {const t = _ as keyof StateDependenciesif (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 = datareturn data}return previous.current},() => {// SSR 安全的初始值return { width: 0, height: 0 }},)return {get width() {stateDependencies.width = truereturn cached.width},get height() {stateDependencies.height = truereturn 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

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。


http://www.wuyegushi.com/news/1255.html

相关文章:

  • 从一起知名线上故障,谈配置灰度发布的重要性
  • Tita 助力618: 制造业行业推行解决方案
  • kubernetes (K8S)集群安装部署
  • PyTorch边界感知上下文神经网络BA-Net在医学图像分割中的应用
  • Qt注册类对象单例与单类型区别
  • 学习笔记:RMAN CATALOG命令手动注册磁带库中的备份片
  • 《构建之法》读后感
  • 达梦增加备份作业 报错-3503 无效的函数参数
  • 读书笔记:Oracle共享池:数据库内存管理的心脏
  • Python类的定义_类和对象的关系_对象的内存模型
  • Python对2028奥运奖牌预测分析:贝叶斯推断、梯度提升机GBM、时间序列、随机森林、二元分类教练效应量化研究
  • 学习笔记:MySQL:Innodb统计信息参数
  • 库卡气体保护焊机器人省气的方法
  • 物联网技术对于农业的运营都起到了哪些作用
  • [07.28学习笔记] Self-attetion Cross-attetion - Luna
  • 【LLM】Transformer各模块PyTorch简单实现Demo
  • 如何在FastAPI中玩转Schema版本管理和灰度发布?
  • C++ Qt开发QUdpSocket网络通信组件
  • fhq-treap学习笔记
  • 7/28
  • Bruce Momjian 深圳 meetup 回顾
  • 贪心
  • sqlite3 本地数据库可视化工具
  • [题解] P5743 【深基7.习8】猴子吃桃
  • gds 格式文档
  • 微服务学习-02-微服务技术栈整理
  • JUC线程池: ScheduledThreadPoolExecutor详解
  • [题解] P5735 【深基7.例1】距离函数
  • uv命令怎么安装并且让gitlab-runner用户可以执行
  • NRF54L15 TAMPC — Tamper controller 作用介绍