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 解决的核心问题是:
- 函数组件没有状态:之前只有类组件能管理状态
- 逻辑复用困难:HOC 和 Render Props 嵌套地狱
- 生命周期复杂:相关逻辑被拆散到不同生命周期方法
本篇会讲清楚:
- 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){// ...}}规则总结:
- 只在最顶层使用 Hook:不要在循环、条件或嵌套函数中调用
- 只在 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// useEffect3.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>}七、易混淆点
- useEffect 执行时机:在浏览器完成布局与绘制后异步执行,不阻塞渲染;useLayoutEffect 在 DOM 变更后同步执行,阻塞渲染。
- 依赖数组:
[]表示只执行一次(首次渲染后);无依赖数组表示每次渲染后都执行。 - 陈旧闭包:useEffect 的闭包捕获的是创建时的值,不是最新的值。解决方案:添加依赖、使用函数式更新、或使用 useRef。
- useMemo vs useCallback:useMemo 缓存计算结果,useCallback 缓存函数引用。
- 自定义 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. 使用 useRef4.什么时候需要使用 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 解决
