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

React Hooks

文章目录

  • 前言
  • 一、Hooks 的基本规则
    • 1.1 两条核心规则
    • 1.2 为什么有这些规则
  • 二、常用 Hooks
    • 2.1 useState
    • 2.2 useEffect
    • 2.3 useContext
  • 三、useEffect vs useLayoutEffect
    • 3.1 执行时机
    • 3.2 执行顺序
    • 3.3 使用场景
  • 四、自定义 Hook
    • 4.1 基本规则
    • 4.2 自定义 Hook 的设计
  • 五、useMemo 与 useCallback
    • 5.1 useMemo:缓存计算结果
    • 5.2 useCallback:缓存函数引用
    • 5.3 滥用问题
  • 六、陈旧闭包问题
    • 6.1 问题描述
    • 6.2 解决方案
  • 七、易混淆点
  • 八、思考与练习
  • 总结

前言

上一篇讲了 Diff 算法;本篇进入React Hooks——这是 React 16.8 引入的重要特性,彻底改变了 React 组件的编写方式。

Hooks 解决的核心问题是:

  1. 函数组件没有状态:之前只有类组件能管理状态
  2. 逻辑复用困难:HOC 和 Render Props 嵌套地狱
  3. 生命周期复杂:相关逻辑被拆散到不同生命周期方法

本篇会讲清楚:

  • Hooks 的基本规则
  • 常用 Hooks 的使用
  • useEffect 与 useLayoutEffect 的区别
  • 自定义 Hook 的设计

一、Hooks 的基本规则

1.1 两条核心规则

// ❌ 错误:在条件语句中调用 HookfunctionComponent({flag}){if(flag){const[count,setCount]=useState(0)// 错误!}}// ❌ 错误:在循环中调用 HookfunctionComponent(){for(leti=0;i<3;i++){const[count,setCount]=useState(0)// 错误!}}// ✅ 正确:始终在组件顶层调用 HookfunctionComponent(){const[count,setCount]=useState(0)const[name,setName]=useState('Alice')// 条件逻辑放在 Hook 之后if(count>10){// ...}}

规则总结

  1. 只在最顶层使用 Hook:不要在循环、条件或嵌套函数中调用
  2. 只在 React 函数组件或自定义 Hook 中调用:不要在普通函数中调用

1.2 为什么有这些规则

// React 内部用数组存储 Hook 状态lethooks=[]letindex=0functionuseState(initialValue){if(hooks[index]===undefined){hooks[index]=initialValue}constcurrentIndex=indexconstsetState=(newValue)=>{hooks[currentIndex]=newValue}return[hooks[index++],setState]}// 每次渲染时,Hook 调用顺序必须一致// 否则 index 会错乱,导致状态混乱

二、常用 Hooks

2.1 useState

const[count,setCount]=useState(0)// 函数式更新(推荐用于依赖旧值的场景)setCount(prev=>prev+1)// 惰性初始化(只在首次渲染执行)const[state,setState]=useState(()=>{returnexpensiveComputation()})

2.2 useEffect

useEffect(()=>{// 副作用逻辑consttimer=setInterval(()=>{console.log('tick')},1000)// 清理函数(在组件卸载或依赖变化前执行)return()=>{clearInterval(timer)}},[deps])// 依赖数组

依赖数组规则

  • 无依赖:每次渲染后都执行
  • 空数组[]:只在首次渲染后执行
  • 有依赖[a, b]:依赖变化后执行

2.3 useContext

constThemeContext=React.createContext('light')functionApp(){return(<ThemeContext.Provider value="dark"><Child/></ThemeContext.Provider>)}functionChild(){consttheme=useContext(ThemeContext)// 'dark'return<div className={theme}>Hello</div>}

三、useEffect vs useLayoutEffect

3.1 执行时机

// useEffect:在浏览器完成布局与绘制后异步执行useEffect(()=>{// 不阻塞浏览器渲染})// useLayoutEffect:在 DOM 变更后同步执行(阻塞渲染)useLayoutEffect(()=>{// 阻塞浏览器渲染,适合需要同步读取 DOM 布局的场景})

3.2 执行顺序

functionComponent(){// 1. 组件函数体执行console.log('render')// 2. DOM 更新// 3. 浏览器绘制useLayoutEffect(()=>{console.log('useLayoutEffect')// 先执行})useEffect(()=>{console.log('useEffect')// 后执行})return<div>Hello</div>}// 输出顺序:// render// useLayoutEffect// useEffect

3.3 使用场景

// ✅ useLayoutEffect:需要同步测量或修改 DOMfunctionTooltip({targetRef}){const[position,setPosition]=useState({x:0,y:0})useLayoutEffect(()=>{constrect=targetRef.current.getBoundingClientRect()setPosition({x:rect.left,y:rect.bottom})},[targetRef])return<div style={{left:position.x,top:position.y}}>Tooltip</div>}// ✅ useEffect:异步副作用(数据请求、订阅、定时器)functionUserProfile({userId}){const[user,setUser]=useState(null)useEffect(()=>{fetchUser(userId).then(setUser)},[userId])return<div>{user?.name}</div>}

四、自定义 Hook

4.1 基本规则

// 自定义 Hook 必须以 "use" 开头functionuseLocalStorage(key,initialValue){const[value,setValue]=useState(()=>{constsaved=localStorage.getItem(key)returnsaved?JSON.parse(saved):initialValue})useEffect(()=>{localStorage.setItem(key,JSON.stringify(value))},[key,value])return[value,setValue]}// 使用functionApp(){const[name,setName]=useLocalStorage('name','Alice')return<input value={name}onChange={e=>setName(e.target.value)}/>}

4.2 自定义 Hook 的设计

// 封装异步请求逻辑functionuseFetch(url){const[data,setData]=useState(null)const[loading,setLoading]=useState(true)const[error,setError]=useState(null)useEffect(()=>{letcancelled=falseconstfetchData=async()=>{setLoading(true)setError(null)try{constresponse=awaitfetch(url)constjson=awaitresponse.json()if(!cancelled){setData(json)}}catch(err){if(!cancelled){setError(err)}}finally{if(!cancelled){setLoading(false)}}}fetchData()// 清理函数:防止组件卸载后更新状态return()=>{cancelled=true}},[url])return{data,loading,error}}// 使用functionUserProfile({userId}){const{data:user,loading,error}=useFetch(`/api/users/${userId}`)if(loading)return<div>Loading...</div>if(error)return<div>Error:{error.message}</div>return<div>{user.name}</div>}

五、useMemo 与 useCallback

5.1 useMemo:缓存计算结果

functionApp({items,filter}){// ❌ 每次渲染都重新计算constfilteredItems=items.filter(item=>item.includes(filter))// ✅ 只在 items 或 filter 变化时重新计算constfilteredItems=useMemo(()=>items.filter(item=>item.includes(filter)),[items,filter])return<List items={filteredItems}/>}

5.2 useCallback:缓存函数引用

functionApp(){const[count,setCount]=useState(0)// ❌ 每次渲染都创建新函数,导致子组件重新渲染consthandleClick=()=>{setCount(count+1)}// ✅ 缓存函数引用,子组件不会因函数变化而重新渲染consthandleClick=useCallback(()=>{setCount(prev=>prev+1)},[])return<Button onClick={handleClick}/>}

5.3 滥用问题

// ❌ 错误:过度优化,简单计算不需要 useMemoconstfullName=useMemo(()=>firstName+' '+lastName,[firstName,lastName])// ✅ 正确:直接计算constfullName=firstName+' '+lastName// ❌ 错误:没有依赖变化的函数不需要 useCallbackconsthandleClick=useCallback(()=>{console.log('click')},[])// 永远不变,但增加了复杂性// ✅ 正确:只有传递给 memo 子组件时才需要constMemoizedChild=React.memo(Child)functionApp(){consthandleClick=useCallback(()=>{console.log('click')},[])return<MemoizedChild onClick={handleClick}/>}

六、陈旧闭包问题

6.1 问题描述

functionCounter(){const[count,setCount]=useState(0)useEffect(()=>{consttimer=setInterval(()=>{console.log(count)// 永远输出 0!setCount(count+1)// 永远设置为 1!},1000)return()=>clearInterval(timer)},[])// 依赖数组为空,闭包捕获的 count 永远是 0return<div>{count}</div>}

6.2 解决方案

// 方案 1:添加依赖useEffect(()=>{consttimer=setInterval(()=>{setCount(count+1)},1000)return()=>clearInterval(timer)},[count])// count 变化时重新创建定时器// 方案 2:使用函数式更新useEffect(()=>{consttimer=setInterval(()=>{setCount(prev=>prev+1)// 使用最新的值},1000)return()=>clearInterval(timer)},[])// 无需依赖 count// 方案 3:使用 useReffunctionCounter(){const[count,setCount]=useState(0)constcountRef=useRef(count)countRef.current=count// 每次渲染更新 refuseEffect(()=>{consttimer=setInterval(()=>{console.log(countRef.current)// 读取最新值setCount(countRef.current+1)},1000)return()=>clearInterval(timer)},[])return<div>{count}</div>}

七、易混淆点

  1. useEffect 执行时机:在浏览器完成布局与绘制后异步执行,不阻塞渲染;useLayoutEffect 在 DOM 变更后同步执行,阻塞渲染。
  2. 依赖数组[]表示只执行一次(首次渲染后);无依赖数组表示每次渲染后都执行。
  3. 陈旧闭包:useEffect 的闭包捕获的是创建时的值,不是最新的值。解决方案:添加依赖、使用函数式更新、或使用 useRef。
  4. useMemo vs useCallback:useMemo 缓存计算结果,useCallback 缓存函数引用
  5. 自定义 Hook:必须以 “use” 开头,本质是复用状态逻辑,不是复用状态本身。

八、思考与练习

1.为什么 Hooks 不能在条件语句中调用?

解析:React 内部用数组存储 Hook 状态,依赖调用顺序定位状态。如果在条件语句中调用,渲染次数不同会导致调用顺序错乱,状态混乱。

2.useEffect 和 useLayoutEffect 的区别是什么?

解析:

  • useEffect:在浏览器完成布局与绘制后异步执行,不阻塞渲染
  • useLayoutEffect:在 DOM 变更后同步执行,阻塞渲染
  • 适用于需要同步测量或修改 DOM 的场景

3.什么是陈旧闭包问题?如何解决?

解析:

// 问题:useEffect 的闭包捕获的是创建时的值useEffect(()=>{consttimer=setInterval(()=>{setCount(count+1)// count 永远是 0},1000)},[])// 解决方案:// 1. 添加依赖 [count]// 2. 使用函数式更新 setCount(prev => prev + 1)// 3. 使用 useRef

4.什么时候需要使用 useMemo?

解析:

  • 复杂计算:过滤、排序、转换大型数组
  • 传递给子组件:避免子组件因引用变化而重新渲染
  • 依赖其他 memoized 值:形成 memoized 链

简单计算(字符串拼接、简单数学运算)不需要使用。

5.如何设计一个好的自定义 Hook?

解析:

  • 单一职责:一个 Hook 只做一件事
  • 命名清晰:以 “use” 开头,语义明确
  • 参数灵活:支持多种配置选项
  • 返回值简洁:返回对象或数组,便于解构
// ✅ 好的设计functionuseFetch(url,options={}){const{immediate=true}=options// ...return{data,loading,error,refetch}}// ❌ 不好的设计functionuseData(url,method,headers,body,cache,retry){// 参数过多,难以维护}

6.useEffect 的清理函数什么时候执行?

解析:

  • 组件卸载时:组件从 DOM 中移除前
  • 依赖变化时:下次 effect 执行前(不是渲染前)
  • 不会在首次渲染后执行:首次渲染没有"上次 effect"需要清理

总结

  • Hooks 规则:只在顶层调用,不在条件/循环中调用
  • useEffect:异步执行,用于副作用(数据请求、订阅、定时器)
  • useLayoutEffect:同步执行,用于需要同步测量/修改 DOM 的场景
  • 自定义 Hook:复用状态逻辑,以 “use” 开头
  • useMemo/useCallback:缓存计算结果/函数引用,避免不必要的重新渲染
  • 陈旧闭包:闭包捕获的是创建时的值,通过依赖、函数式更新或 useRef 解决
http://www.rkmt.cn/news/1494559.html

相关文章:

  • SaltStack中state的变量
  • MemcardRex技术解析:PS1游戏存档管理的架构设计与应用实践
  • Xenia Canary:跨架构实时编译的技术革命与开源创新
  • 告别IDM试用期烦恼:开源脚本让你的下载管理体验永久免费
  • 跨境电商图片翻译工具市场报告:2026趋势与机会
  • 3个Windows维护痛点,Dism++一站式解决指南
  • 如何在Windows电脑上安装安卓应用:3分钟学会APK安装器的终极指南
  • KE15Z/14Z外部晶振与SWD接口硬件设计实战指南
  • 2026年CSDN年度技术趋势预测:AI原生、云原生与开发者工具新范式
  • GPT-4的2%激活率:MoE稀疏架构原理与工程实践
  • 别再只搜Star数了!手把手教你用GitHub Topics和高级搜索,精准发现宝藏项目
  • 华硕笔记本性能调校神器:5分钟掌握G-Helper完整使用指南
  • 嵌入式学习随记
  • GetQzonehistory:如何完整备份QQ空间说说,守护你的数字记忆
  • 深入解析NXP i.MX 6系列处理器:架构、外设与嵌入式开发实战
  • 3步解锁中兴光猫隐藏功能:zteOnu工具完全指南
  • 嵌入式设计实战:基于ARM Cortex-M4的K20 MCU数据手册深度解析与应用指南
  • ONNX Runtime模型部署优化:从导出到推理加速的全链路实践
  • 静物摄影二次创作,image2 重塑光影氛围
  • CMake详细
  • 别再手动加ORCID了!用LaTeX在Overleaf里一键搞定作者标识(附完整代码)
  • 郑州OPC哪个公司好
  • 保姆级教程:从Anaconda安装到策略回测,手把手带你跑通第一个掘金量化策略
  • 深度解析开源多显示器亮度管理方案:Monitorian架构设计与实战应用
  • ComfyUI-Impact-Pack终极指南:5分钟掌握AI图像增强神器
  • 2026年工程项目管理软件测评:洁净工程的关键一战
  • Point-E技术如何革新3D内容创作:从文本到点云的智能生成实战指南
  • 从‘水球’到‘地球’:CESM模式复杂度升级全流程解析(含AMIP/CMIP测试指南)
  • 别再只盯着TPM 2.0了!从国产TPCM实战出发,聊聊可信启动的静态度量链到底怎么搭
  • MCU时钟与模拟外设电气参数深度解析:从数据手册到设计实战