文章目录
- 前言
- [一、Hooks 的基本规则](#一、Hooks 的基本规则)
-
- [1.1 两条核心规则](#1.1 两条核心规则)
- [1.2 为什么有这些规则](#1.2 为什么有这些规则)
- [二、常用 Hooks](#二、常用 Hooks)
-
- [2.1 useState](#2.1 useState)
- [2.2 useEffect](#2.2 useEffect)
- [2.3 useContext](#2.3 useContext)
- [三、useEffect vs useLayoutEffect](#三、useEffect vs useLayoutEffect)
-
- [3.1 执行时机](#3.1 执行时机)
- [3.2 执行顺序](#3.2 执行顺序)
- [3.3 使用场景](#3.3 使用场景)
- [四、自定义 Hook](#四、自定义 Hook)
-
- [4.1 基本规则](#4.1 基本规则)
- [4.2 自定义 Hook 的设计](#4.2 自定义 Hook 的设计)
- [五、useMemo 与 useCallback](#五、useMemo 与 useCallback)
-
- [5.1 useMemo:缓存计算结果](#5.1 useMemo:缓存计算结果)
- [5.2 useCallback:缓存函数引用](#5.2 useCallback:缓存函数引用)
- [5.3 滥用问题](#5.3 滥用问题)
- 六、陈旧闭包问题
-
- [6.1 问题描述](#6.1 问题描述)
- [6.2 解决方案](#6.2 解决方案)
- 七、易混淆点
- 八、思考与练习
- 总结
前言
上一篇讲了 Diff 算法;本篇进入 React Hooks------这是 React 16.8 引入的重要特性,彻底改变了 React 组件的编写方式。
Hooks 解决的核心问题是:
- 函数组件没有状态:之前只有类组件能管理状态
- 逻辑复用困难:HOC 和 Render Props 嵌套地狱
- 生命周期复杂:相关逻辑被拆散到不同生命周期方法
本篇会讲清楚:
- Hooks 的基本规则
- 常用 Hooks 的使用
- useEffect 与 useLayoutEffect 的区别
- 自定义 Hook 的设计
一、Hooks 的基本规则
1.1 两条核心规则
javascript
// ❌ 错误:在条件语句中调用 Hook
function Component({ flag }) {
if (flag) {
const [count, setCount] = useState(0) // 错误!
}
}
// ❌ 错误:在循环中调用 Hook
function Component() {
for (let i = 0; i < 3; i++) {
const [count, setCount] = useState(0) // 错误!
}
}
// ✅ 正确:始终在组件顶层调用 Hook
function Component() {
const [count, setCount] = useState(0)
const [name, setName] = useState('Alice')
// 条件逻辑放在 Hook 之后
if (count > 10) {
// ...
}
}
规则总结:
- 只在最顶层使用 Hook:不要在循环、条件或嵌套函数中调用
- 只在 React 函数组件或自定义 Hook 中调用:不要在普通函数中调用
1.2 为什么有这些规则
javascript
// React 内部用数组存储 Hook 状态
let hooks = []
let index = 0
function useState(initialValue) {
if (hooks[index] === undefined) {
hooks[index] = initialValue
}
const currentIndex = index
const setState = (newValue) => {
hooks[currentIndex] = newValue
}
return [hooks[index++], setState]
}
// 每次渲染时,Hook 调用顺序必须一致
// 否则 index 会错乱,导致状态混乱
二、常用 Hooks
2.1 useState
javascript
const [count, setCount] = useState(0)
// 函数式更新(推荐用于依赖旧值的场景)
setCount(prev => prev + 1)
// 惰性初始化(只在首次渲染执行)
const [state, setState] = useState(() => {
return expensiveComputation()
})
2.2 useEffect
javascript
useEffect(() => {
// 副作用逻辑
const timer = setInterval(() => {
console.log('tick')
}, 1000)
// 清理函数(在组件卸载或依赖变化前执行)
return () => {
clearInterval(timer)
}
}, [deps]) // 依赖数组
依赖数组规则:
- 无依赖:每次渲染后都执行
- 空数组
[]:只在首次渲染后执行 - 有依赖
[a, b]:依赖变化后执行
2.3 useContext
javascript
const ThemeContext = React.createContext('light')
function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
)
}
function Child() {
const theme = useContext(ThemeContext) // 'dark'
return <div className={theme}>Hello</div>
}
三、useEffect vs useLayoutEffect
3.1 执行时机
javascript
// useEffect:在浏览器完成布局与绘制后异步执行
useEffect(() => {
// 不阻塞浏览器渲染
})
// useLayoutEffect:在 DOM 变更后同步执行(阻塞渲染)
useLayoutEffect(() => {
// 阻塞浏览器渲染,适合需要同步读取 DOM 布局的场景
})
3.2 执行顺序
javascript
function Component() {
// 1. 组件函数体执行
console.log('render')
// 2. DOM 更新
// 3. 浏览器绘制
useLayoutEffect(() => {
console.log('useLayoutEffect') // 先执行
})
useEffect(() => {
console.log('useEffect') // 后执行
})
return <div>Hello</div>
}
// 输出顺序:
// render
// useLayoutEffect
// useEffect
3.3 使用场景
javascript
// ✅ useLayoutEffect:需要同步测量或修改 DOM
function Tooltip({ targetRef }) {
const [position, setPosition] = useState({ x: 0, y: 0 })
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect()
setPosition({ x: rect.left, y: rect.bottom })
}, [targetRef])
return <div style={{ left: position.x, top: position.y }}>Tooltip</div>
}
// ✅ useEffect:异步副作用(数据请求、订阅、定时器)
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetchUser(userId).then(setUser)
}, [userId])
return <div>{user?.name}</div>
}
四、自定义 Hook
4.1 基本规则
javascript
// 自定义 Hook 必须以 "use" 开头
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key)
return saved ? JSON.parse(saved) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
}
// 使用
function App() {
const [name, setName] = useLocalStorage('name', 'Alice')
return <input value={name} onChange={e => setName(e.target.value)} />
}
4.2 自定义 Hook 的设计
javascript
// 封装异步请求逻辑
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
const fetchData = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
const json = await response.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 }
}
// 使用
function UserProfile({ 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:缓存计算结果
javascript
function App({ items, filter }) {
// ❌ 每次渲染都重新计算
const filteredItems = items.filter(item => item.includes(filter))
// ✅ 只在 items 或 filter 变化时重新计算
const filteredItems = useMemo(
() => items.filter(item => item.includes(filter)),
[items, filter]
)
return <List items={filteredItems} />
}
5.2 useCallback:缓存函数引用
javascript
function App() {
const [count, setCount] = useState(0)
// ❌ 每次渲染都创建新函数,导致子组件重新渲染
const handleClick = () => {
setCount(count + 1)
}
// ✅ 缓存函数引用,子组件不会因函数变化而重新渲染
const handleClick = useCallback(() => {
setCount(prev => prev + 1)
}, [])
return <Button onClick={handleClick} />
}
5.3 滥用问题
javascript
// ❌ 错误:过度优化,简单计算不需要 useMemo
const fullName = useMemo(() => firstName + ' ' + lastName, [firstName, lastName])
// ✅ 正确:直接计算
const fullName = firstName + ' ' + lastName
// ❌ 错误:没有依赖变化的函数不需要 useCallback
const handleClick = useCallback(() => {
console.log('click')
}, []) // 永远不变,但增加了复杂性
// ✅ 正确:只有传递给 memo 子组件时才需要
const MemoizedChild = React.memo(Child)
function App() {
const handleClick = useCallback(() => {
console.log('click')
}, [])
return <MemoizedChild onClick={handleClick} />
}
六、陈旧闭包问题
6.1 问题描述
javascript
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log(count) // 永远输出 0!
setCount(count + 1) // 永远设置为 1!
}, 1000)
return () => clearInterval(timer)
}, []) // 依赖数组为空,闭包捕获的 count 永远是 0
return <div>{count}</div>
}
6.2 解决方案
javascript
// 方案 1:添加依赖
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(timer)
}, [count]) // count 变化时重新创建定时器
// 方案 2:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1) // 使用最新的值
}, 1000)
return () => clearInterval(timer)
}, []) // 无需依赖 count
// 方案 3:使用 useRef
function Counter() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
countRef.current = count // 每次渲染更新 ref
useEffect(() => {
const timer = 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. 什么是陈旧闭包问题?如何解决?
解析:
javascript
// 问题:useEffect 的闭包捕获的是创建时的值
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // count 永远是 0
}, 1000)
}, [])
// 解决方案:
// 1. 添加依赖 [count]
// 2. 使用函数式更新 setCount(prev => prev + 1)
// 3. 使用 useRef
4. 什么时候需要使用 useMemo?
解析:
- 复杂计算:过滤、排序、转换大型数组
- 传递给子组件:避免子组件因引用变化而重新渲染
- 依赖其他 memoized 值:形成 memoized 链
简单计算(字符串拼接、简单数学运算)不需要使用。
5. 如何设计一个好的自定义 Hook?
解析:
- 单一职责:一个 Hook 只做一件事
- 命名清晰:以 "use" 开头,语义明确
- 参数灵活:支持多种配置选项
- 返回值简洁:返回对象或数组,便于解构
javascript
// ✅ 好的设计
function useFetch(url, options = {}) {
const { immediate = true } = options
// ...
return { data, loading, error, refetch }
}
// ❌ 不好的设计
function useData(url, method, headers, body, cache, retry) {
// 参数过多,难以维护
}
6. useEffect 的清理函数什么时候执行?
解析:
- 组件卸载时:组件从 DOM 中移除前
- 依赖变化时:下次 effect 执行前(不是渲染前)
- 不会在首次渲染后执行:首次渲染没有"上次 effect"需要清理
总结
- Hooks 规则:只在顶层调用,不在条件/循环中调用
- useEffect:异步执行,用于副作用(数据请求、订阅、定时器)
- useLayoutEffect:同步执行,用于需要同步测量/修改 DOM 的场景
- 自定义 Hook:复用状态逻辑,以 "use" 开头
- useMemo/useCallback:缓存计算结果/函数引用,避免不必要的重新渲染
- 陈旧闭包:闭包捕获的是创建时的值,通过依赖、函数式更新或 useRef 解决