React Hooks 深度解析
Hooks 让函数组件拥有状态和副作用能力,底层基于 Fiber 节点上的链表结构。
Hooks 底层原理
每个函数组件对应一个 Fiber 节点,Hooks 以链表形式存储在 fiber.memoizedState 上:
fiber.memoizedState
→ Hook1 (useState) → Hook2 (useEffect) → Hook3 (useRef) → null
{ state: 0 } { effect: fn } { ref: {} }
这就是为什么 Hooks 不能在条件语句中调用:
顺序必须在每次渲染中保持一致,否则链表对应关系会错乱。useState 深度
jsx
// useState 的更新是异步批处理的(React 18 自动批处理)
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
// React 18:这三次更新会被批处理为一次渲染
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
// 结果:count = 1(不是 3!因为 count 是闭包捕获的旧值)
}
function handleClickCorrect() {
// ✅ 函数式更新:基于最新状态
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// 结果:count = 3 ✓
}
return <button onClick={handleClickCorrect}>{count}</button>
}
// 惰性初始化:避免每次渲染都执行昂贵计算
function App() {
// ❌ 每次渲染都调用 expensiveCalc()
const [state, setState] = useState(expensiveCalc())
// ✅ 只在初始化时调用一次
const [state, setState] = useState(() => expensiveCalc())
}useEffect 完全指南
jsx
// 依赖数组的三种形式
useEffect(() => {
// 每次渲染后都执行
})
useEffect(() => {
// 只在挂载时执行一次
}, [])
useEffect(() => {
// dep1 或 dep2 变化时执行
}, [dep1, dep2])
// 清理函数
useEffect(() => {
const subscription = subscribe(userId)
return () => {
subscription.unsubscribe() // 组件卸载或下次 effect 执行前调用
}
}, [userId])
// 常见模式:数据获取
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false // 防止竞态条件
setLoading(true)
fetchUser(userId).then(data => {
if (!cancelled) {
setUser(data)
setLoading(false)
}
})
return () => { cancelled = true } // 清理:取消过期请求
}, [userId])
if (loading) return <Spinner />
return <div>{user?.name}</div>
}useRef
jsx
// 两种用途:
// 1. 访问 DOM 元素
function TextInput() {
const inputRef = useRef(null)
function focusInput() {
inputRef.current.focus()
}
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>聚焦</button>
</>
)
}
// 2. 保存不触发重渲染的可变值
function Timer() {
const [count, setCount] = useState(0)
const intervalRef = useRef(null)
function start() {
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1)
}, 1000)
}
function stop() {
clearInterval(intervalRef.current)
}
// 保存上一次的值
function usePrevious(value) {
const ref = useRef()
useEffect(() => { ref.current = value })
return ref.current
}
}useMemo & useCallback
jsx
// useMemo:缓存计算结果
function ProductList({ products, filter }) {
// ✅ 只有 products 或 filter 变化时才重新计算
const filtered = useMemo(
() => products.filter(p => p.category === filter),
[products, filter]
)
return filtered.map(p => <ProductCard key={p.id} product={p} />)
}
// useCallback:缓存函数引用
function Parent() {
const [count, setCount] = useState(0)
// ✅ 函数引用稳定,Child 不会因为 Parent 重渲染而重渲染
const handleClick = useCallback(() => {
console.log('clicked')
}, []) // 无依赖,永远是同一个函数
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child onClick={handleClick} />
</>
)
}
// React.memo:跳过 props 未变化的重渲染
const Child = React.memo(function Child({ onClick }) {
console.log('Child 渲染')
return <button onClick={onClick}>子按钮</button>
})
// ⚠️ 不要过度优化:
// - useMemo/useCallback 本身有开销
// - 只在确实有性能问题时才用
// - 先用 React DevTools Profiler 定位瓶颈useReducer
jsx
// 适合复杂状态逻辑
const initialState = { count: 0, step: 1, history: [] }
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + state.step,
history: [...state.history, state.count]
}
case 'decrement':
return { ...state, count: state.count - state.step }
case 'setStep':
return { ...state, step: action.payload }
case 'reset':
return initialState
default:
throw new Error(`未知 action: ${action.type}`)
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: +e.target.value })}
/>
</div>
)
}自定义 Hook
jsx
// 封装数据获取逻辑
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
fetch(url)
.then(res => res.json())
.then(data => { if (!cancelled) { setData(data); setLoading(false) } })
.catch(err => { if (!cancelled) { setError(err); setLoading(false) } })
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// 封装表单逻辑
function useForm(initialValues) {
const [values, setValues] = useState(initialValues)
const [errors, setErrors] = useState({})
const handleChange = useCallback((e) => {
const { name, value } = e.target
setValues(prev => ({ ...prev, [name]: value }))
}, [])
const reset = useCallback(() => {
setValues(initialValues)
setErrors({})
}, [initialValues])
return { values, errors, setErrors, handleChange, reset }
}
// 使用
function LoginForm() {
const { values, handleChange } = useForm({ email: '', password: '' })
const { data, loading } = useFetch('/api/user')
return (
<form>
<input name="email" value={values.email} onChange={handleChange} />
<input name="password" value={values.password} onChange={handleChange} />
</form>
)
}useContext
jsx
// 创建 Context
const ThemeContext = createContext({ theme: 'light', toggle: () => {} })
// Provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const value = useMemo(() => ({
theme,
toggle: () => setTheme(t => t === 'light' ? 'dark' : 'light')
}), [theme])
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
// 自定义 Hook 封装 Context 使用
function useTheme() {
const context = useContext(ThemeContext)
if (!context) throw new Error('useTheme 必须在 ThemeProvider 内使用')
return context
}
// 消费
function Button() {
const { theme, toggle } = useTheme()
return <button className={theme} onClick={toggle}>切换主题</button>
}Hooks 使用规则
- 只在函数组件或自定义 Hook 中调用
- 只在顶层调用(不能在条件、循环、嵌套函数中)
- 自定义 Hook 以
use开头 - 依赖数组要诚实:用到的值都要放进去(用 eslint-plugin-react-hooks 检查)