React Hooks 核心概念与实战面试题全解析
一、核心要点速览
💡 核心考点
- Hooks 本质: 让函数组件拥有状态和副作用能力
- 使用规则: 只能在顶层调用,不能在条件/循环中调用
- 常用 Hooks: useState、useEffect、useContext、useReducer
- 性能优化: useCallback、useMemo、useRef
- 自定义 Hooks: 逻辑复用的最佳实践
二、Hooks 基础概念
1. 为什么需要 Hooks?
javascript
// ========== Class Component 的问题 ==========
// 问题 1: 逻辑复用困难(需要 HOC 或 Render Props)
class UserFetcher extends React.Component {
state = { user: null, loading: true }
componentDidMount() {
fetchUser(this.props.userId).then(user => {
this.setState({ user, loading: false })
})
}
render() {
return this.props.children(this.state)
}
}
// 使用时嵌套严重
<UserFetcher userId={1}>
{({ user, loading }) => (
<PostFetcher userId={1}>
{({ posts }) => (
<CommentFetcher postId={posts[0].id}>
{({ comments }) => (
<div>{/* 回调地狱 */}</div>
)}
</CommentFetcher>
)}
</PostFetcher>
)}
</UserFetcher>
// 问题 2: 复杂组件难以理解
class ComplexComponent extends React.Component {
componentDidMount() {
// 订阅事件
window.addEventListener('resize', this.handleResize)
// 获取数据
this.fetchData()
// 启动定时器
this.timer = setInterval(this.tick, 1000)
}
componentDidUpdate(prevProps) {
// 同样的逻辑又要写一遍
if (prevProps.userId !== this.props.userId) {
this.fetchData()
}
}
componentWillUnmount() {
// 清理逻辑分散
window.removeEventListener('resize', this.handleResize)
clearInterval(this.timer)
}
render() { /* ... */ }
}
// ========== Hooks 解决方案 ==========
// 解决 1: 逻辑复用 - 自定义 Hooks
function useUser(userId) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchUser(userId).then(user => {
setUser(user)
setLoading(false)
})
}, [userId])
return { user, loading }
}
// 使用时清晰简洁
function UserProfile({ userId }) {
const { user, loading } = useUser(userId)
const { posts } = usePosts(userId)
const { comments } = useComments(posts[0]?.id)
if (loading) return <Spinner />
return <div>{/* 清晰的逻辑 */}</div>
}
// 解决 2: 相关逻辑聚合
function ComplexComponent({ userId }) {
// 窗口大小逻辑在一起
useEffect(() => {
const handleResize = () => console.log(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// 数据获取逻辑在一起
useEffect(() => {
fetchData(userId)
}, [userId])
// 定时器逻辑在一起
useEffect(() => {
const timer = setInterval(tick, 1000)
return () => clearInterval(timer)
}, [])
return <div>{/* ... */}</div>
}2. Hooks 执行流程图
┌──────────────────────────────────────────────────────────┐
│ React Hooks 执行流程 │
└──────────────────────────────────────────────────────────┘
首次渲染:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function Counter() {
const [count, setCount] = useState(0) // Hook 1
useEffect(() => { // Hook 2
document.title = `Count: ${count}`
}, [count])
return <button onClick={() => setCount(count + 1)}>
{count}
</button>
}
内部结构:
┌─────────────────────────────────────┐
│ Fiber Node │
│ ┌───────────────────────────────┐ │
│ │ memoizedState (Hook 链表) │ │
│ │ │ │
│ │ Hook 1: │ │
│ │ - memoizedState: 0 │ │
│ │ - queue: { pending... } │ │
│ │ - next: ────────────────┐ │ │
│ │ │ │ │
│ │ Hook 2: │ │ │
│ │ - memoizedState: null │ │ │
│ │ - effect: { │ │ │
│ │ create: fn, │ │ │
│ │ destroy: undefined, │ │ │
│ │ deps: [count] │ │ │
│ │ } │ │ │
│ │ - next: null ◄──────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
更新渲染:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
用户点击按钮 → setCount(1)
│
▼
┌─────────────────┐
│ 调度重新渲染 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 遍历 Hook 链表 │ ← 按顺序读取上次的状态
└────────┬────────┘
│
├─ Hook 1: 读取 count = 0
│ 更新为 1
│
└─ Hook 2: 比较 deps [0] vs [1]
不同 → 执行 cleanup
执行新 effect
关键点:
✓ Hooks 基于链表存储状态
✓ 必须按相同顺序调用
✓ 依赖数组浅比较
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━三、常用 Hooks 详解
1. useState - 状态管理
javascript
// ========== 基础用法 ==========
function Counter() {
const [count, setCount] = useState(0)
// 直接更新
const increment = () => setCount(count + 1)
// 函数式更新(推荐,基于最新状态)
const incrementSafe = () => setCount(prev => prev + 1)
return <button onClick={incrementSafe}>{count}</button>
}
// ========== 惰性初始化 ==========
function ExpensiveComponent() {
// ✓ 只在初始渲染时计算一次
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props)
return initialState
})
// ✗ 每次渲染都计算(浪费性能)
const [state, setState] = useState(someExpensiveComputation(props))
}
// ========== 常见陷阱 ==========
// 陷阱 1: 异步更新
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
console.log(count) // 0(还是旧值)
}
// 结果: count = 1(不是 3)
// ✓ 正确做法
const handleClickCorrect = () => {
setCount(c => c + 1)
setCount(c => c + 1)
setCount(c => c + 1)
}
// 结果: count = 3
}
// 陷阱 2: 对象状态更新
function Form() {
const [form, setForm] = useState({ name: '', age: 0 })
const updateName = (name) => {
// ✗ 错误:丢失其他字段
setForm({ name })
// ✓ 正确:展开运算符
setForm(prev => ({ ...prev, name }))
}
}
// 陷阱 3: 闭包陷阱
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log(count) // 永远是 0(闭包捕获)
}, 1000)
return () => clearInterval(id)
}, []) // 空依赖数组
// ✓ 解决方案 1: 添加依赖
useEffect(() => {
const id = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(id)
}, [count]) // 每次 count 变化都重建定时器
// ✓ 解决方案 2: 函数式更新
useEffect(() => {
const id = setInterval(() => {
setCount(c => {
console.log(c) // 最新值
return c + 1
})
}, 1000)
return () => clearInterval(id)
}, [])
}2. useEffect - 副作用处理
javascript
// ========== 基本语法 ==========
useEffect(() => {
// 副作用代码
const subscription = subscribeToData()
// 清理函数(可选)
return () => {
subscription.unsubscribe()
}
}, [dependencies]) // 依赖数组
// ========== 三种模式 ==========
// 模式 1: 每次渲染后执行(无依赖数组)
useEffect(() => {
console.log('每次渲染都执行')
})
// 模式 2: 仅挂载和卸载时执行(空依赖数组)
useEffect(() => {
console.log('仅挂载时执行')
return () => {
console.log('仅卸载时执行')
}
}, [])
// 模式 3: 依赖变化时执行
useEffect(() => {
console.log('userId 变化时执行')
fetchUser(userId)
}, [userId])
// ========== 实际应用场景 ==========
// 场景 1: 数据获取
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
async function fetchData() {
try {
setLoading(true)
const data = await fetchUser(userId)
if (!cancelled) {
setUser(data)
setError(null)
}
} catch (err) {
if (!cancelled) {
setError(err)
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
fetchData()
return () => {
cancelled = true // 防止组件卸载后更新状态
}
}, [userId])
if (loading) return <Spinner />
if (error) return <Error message={error.message} />
return <div>{user.name}</div>
}
// 场景 2: 订阅/取消订阅
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId)
connection.connect()
return () => {
connection.disconnect()
}
}, [roomId])
return <ChatUI />
}
// 场景 3: DOM 操作
function AutoFocusInput() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />
}
// 场景 4: 事件监听
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}
// ========== 常见错误 ==========
// 错误 1: 忘记添加依赖
function SearchResults({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
// ✗ query 变化不会重新请求
fetch(`/api/search?q=${query}`).then(setResults)
}, []) // 应该包含 query
// ✓ 正确
useEffect(() => {
fetch(`/api/search?q=${query}`).then(setResults)
}, [query])
}
// 错误 2: 依赖过多导致无限循环
function Counter() {
const [count, setCount] = useState(0)
const [data, setData] = useState(null)
// ✗ 无限循环:setData 每次都是新引用
useEffect(() => {
fetchData().then(setData)
}, [data])
// ✓ 正确:只依赖必要的值
useEffect(() => {
fetchData().then(setData)
}, []) // 仅在挂载时执行
}
// 错误 3: 在 effect 中修改状态触发循环
function BadExample() {
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count + 1) // 触发重新渲染 → 再次执行 effect → 无限循环
}, [count])
}3. useContext - 跨组件通信
javascript
// ========== 基础用法 ==========
// 1. 创建 Context
const ThemeContext = React.createContext('light')
const UserContext = React.createContext(null)
// 2. 提供值
function App() {
const [theme, setTheme] = useState('dark')
const [user, setUser] = useState({ name: 'Vue' })
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
<MainContent />
</UserContext.Provider>
</ThemeContext.Provider>
)
}
// 3. 消费值
function ThemedButton() {
const theme = useContext(ThemeContext)
const user = useContext(UserContext)
return (
<button className={`btn-${theme}`}>
Hello, {user.name}
</button>
)
}
// ========== 性能优化 ==========
// 问题:Provider 值变化会导致所有消费者重新渲染
function App() {
// ✗ 每次渲染都创建新对象,导致不必要的重渲染
return (
<UserContext.Provider value={{ name: 'Vue', age: 3 }}>
<Child />
</UserContext.Provider>
)
}
// ✓ 使用 useMemo 缓存
function App() {
const userValue = useMemo(() => ({ name: 'Vue', age: 3 }), [])
return (
<UserContext.Provider value={userValue}>
<Child />
</UserContext.Provider>
)
}
// ========== 自定义 Hook 封装 ==========
function useTheme() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}4. useReducer - 复杂状态管理
javascript
// ========== 基础用法 ==========
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return initialState
default:
throw new Error(`Unknown action: ${action.type}`)
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
)
}
// ========== 惰性初始化 ==========
function init(initialCount) {
return { count: initialCount }
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return init(action.payload)
default:
throw new Error()
}
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
Reset
</button>
</>
)
}
// ========== 与 useContext 结合(替代 Redux)==========
// actions.js
export const todoActions = {
addTodo: (text) => ({ type: 'ADD_TODO', payload: text }),
toggleTodo: (id) => ({ type: 'TOGGLE_TODO', payload: id }),
deleteTodo: (id) => ({ type: 'DELETE_TODO', payload: id })
}
// reducer.js
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.payload, completed: false }]
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
)
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload)
default:
return state
}
}
// TodoContext.js
const TodoContext = React.createContext()
export function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, [])
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
)
}
export function useTodos() {
const context = useContext(TodoContext)
if (!context) {
throw new Error('useTodos must be used within TodoProvider')
}
return context
}
// TodoList.js
function TodoList() {
const { todos, dispatch } = useTodos()
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch(todoActions.toggleTodo(todo.id))}
>
{todo.text}
</span>
<button onClick={() => dispatch(todoActions.deleteTodo(todo.id))}>
Delete
</button>
</li>
))}
</ul>
)
}四、性能优化 Hooks
1. useCallback - 缓存回调函数
javascript
// ========== 问题场景 ==========
function Parent() {
const [count, setCount] = useState(0)
// ✗ 每次渲染都创建新函数
const handleClick = () => {
console.log('clicked')
}
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
)
}
function Child({ onClick }) {
// 即使 props 没变,也会重新渲染(因为 onClick 引用变了)
console.log('Child rendered')
return <button onClick={onClick}>Click me</button>
}
// ========== 解决方案 ==========
function Parent() {
const [count, setCount] = useState(0)
// ✓ 只有依赖变化时才创建新函数
const handleClick = useCallback(() => {
console.log('clicked')
}, []) // 空依赖,函数引用永远不变
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
)
}
// 配合 React.memo 使用
const Child = React.memo(function Child({ onClick }) {
console.log('Child rendered')
return <button onClick={onClick}>Click me</button>
})
// ========== 带依赖的 useCallback ==========
function SearchBox({ onSearch }) {
const [query, setQuery] = useState('')
// ✓ 依赖 query,query 变化时更新回调
const handleSearch = useCallback(() => {
onSearch(query)
}, [query, onSearch])
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
)
}
// ========== 常见误区 ==========
// 误区 1: 滥用 useCallback
function Component() {
// ✗ 没必要:函数没有传递给子组件
const handleClick = useCallback(() => {
console.log('click')
}, [])
// ✓ 普通函数即可
const handleClick = () => {
console.log('click')
}
}
// 原则:只有传递给子组件或作为依赖时才用 useCallback
// 误区 2: 忘记添加依赖
function Component({ userId }) {
// ✗ userId 变化但回调不更新
const fetchUser = useCallback(() => {
api.getUser(userId)
}, []) // 缺少 userId
// ✓ 正确
const fetchUser = useCallback(() => {
api.getUser(userId)
}, [userId])
}2. useMemo - 缓存计算结果
javascript
// ========== 基础用法 ==========
function ExpensiveComponent({ items, filter }) {
// ✗ 每次渲染都重新计算
const filteredItems = items.filter(item => item.includes(filter))
// ✓ 只有依赖变化时才重新计算
const filteredItems = useMemo(() => {
return items.filter(item => item.includes(filter))
}, [items, filter])
return <List items={filteredItems} />
}
// ========== 实际应用场景 ==========
// 场景 1: 复杂计算
function DataTable({ data, sortBy }) {
const sortedData = useMemo(() => {
console.log('排序计算...')
return [...data].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name)
if (sortBy === 'age') return a.age - b.age
return 0
})
}, [data, sortBy])
return <Table data={sortedData} />
}
// 场景 2: 创建对象引用
function UserProfile({ user }) {
// ✗ 每次渲染创建新对象,导致子组件无效重渲染
const style = { color: user.active ? 'green' : 'red' }
// ✓ 缓存对象引用
const style = useMemo(() => ({
color: user.active ? 'green' : 'red'
}), [user.active])
return <div style={style}>{user.name}</div>
}
// 场景 3: 避免重复 API 调用
function SearchResults({ query }) {
const fetchResults = useMemo(() => {
return debounce(async (q) => {
const results = await api.search(q)
setResults(results)
}, 300)
}, []) // 只创建一次 debounced 函数
useEffect(() => {
fetchResults(query)
}, [query, fetchResults])
}
// ========== 性能对比 ==========
function Benchmark() {
const [count, setCount] = useState(0)
const items = Array.from({ length: 10000 }, (_, i) => i)
// 不使用 useMemo
const start1 = performance.now()
const result1 = items.filter(i => i % 2 === 0)
const time1 = performance.now() - start1
// 使用 useMemo
const start2 = performance.now()
const result2 = useMemo(() => {
return items.filter(i => i % 2 === 0)
}, [items])
const time2 = performance.now() - start2
return (
<div>
<p>Without useMemo: {time1.toFixed(2)}ms</p>
<p>With useMemo: {time2.toFixed(2)}ms (后续渲染接近 0ms)</p>
<button onClick={() => setCount(count + 1)}>Re-render</button>
</div>
)
}3. useRef - 持久化引用
javascript
// ========== 基础用法 ==========
function TextInput() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
return <input ref={inputRef} />
}
// ========== 保存可变值(不触发重渲染)==========
function Timer() {
const [count, setCount] = useState(0)
const intervalRef = useRef(null)
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
const stopTimer = () => {
clearInterval(intervalRef.current)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={stopTimer}>Stop</button>
</div>
)
}
// ========== 记录前一个值 ==========
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>Now: {count}, before: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
// ========== 避免不必要的 Effect 执行 ==========
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('')
const isFirstRender = useRef(true)
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false
return
}
// 跳过首次渲染
onSearch(query)
}, [query, onSearch])
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
// ========== useRef vs useState ==========
function Comparison() {
// useState: 更新触发重渲染
const [countState, setCountState] = useState(0)
// useRef: 更新不触发重渲染
const countRef = useRef(0)
const incrementState = () => setCountState(countState + 1)
const incrementRef = () => {
countRef.current += 1
console.log(countRef.current) // 立即看到新值
}
return (
<div>
<p>State: {countState}(更新会重渲染)</p>
<p>Ref: {countRef.current}(更新不会重渲染,显示的是旧值)</p>
<button onClick={incrementState}>State++</button>
<button onClick={incrementRef}>Ref++</button>
</div>
)
}五、自定义 Hooks
1. 自定义 Hooks 规范
javascript
// ========== 命名规范 ==========
// ✓ 必须以 use 开头
function useUserData() { /* ... */ }
function useFetch() { /* ... */ }
// ✗ 错误命名
function getUserData() { /* ... */ }
function fetchData() { /* ... */ }
// ========== 基本结构 ==========
function useCustomHook(initialValue) {
// 1. 声明状态
const [state, setState] = useState(initialValue)
// 2. 副作用
useEffect(() => {
// 副作用逻辑
}, [])
// 3. 缓存回调
const callback = useCallback(() => {
// 回调逻辑
}, [])
// 4. 返回值
return {
state,
setState,
callback
}
}
// ========== 组合使用其他 Hooks ==========
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return width
}
function useDocumentTitle(title) {
useEffect(() => {
document.title = title
}, [title])
}
function MyComponent() {
const width = useWindowWidth()
useDocumentTitle(`Width: ${width}`)
return <div>Window width: {width}</div>
}2. 实用自定义 Hooks
javascript
// ========== useLocalStorage ==========
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue]
}
// 使用
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
)
}
// ========== useDebounce ==========
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}
// 使用
function SearchBox() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
if (debouncedQuery) {
fetchResults(debouncedQuery)
}
}, [debouncedQuery])
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
)
}
// ========== useAsync ==========
function useAsync(asyncFunction, immediate = true) {
const [status, setStatus] = useState('idle')
const [value, setValue] = useState(null)
const [error, setError] = useState(null)
const execute = useCallback(() => {
setStatus('pending')
setValue(null)
setError(null)
return asyncFunction()
.then(response => {
setValue(response)
setStatus('success')
})
.catch(error => {
setError(error)
setStatus('error')
})
}, [asyncFunction])
useEffect(() => {
if (immediate) {
execute()
}
}, [execute, immediate])
return { execute, status, value, error }
}
// 使用
function UserProfile({ userId }) {
const { status, value: user, error, execute } = useAsync(
() => fetchUser(userId),
false
)
useEffect(() => {
execute()
}, [userId, execute])
if (status === 'pending') return <Spinner />
if (status === 'error') return <Error message={error.message} />
if (status === 'success') return <div>{user.name}</div>
return null
}
// ========== useEventListener ==========
function useEventListener(eventName, handler, element = window) {
const savedHandler = useRef()
useEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
const isSupported = element && element.addEventListener
if (!isSupported) return
const eventListener = (event) => savedHandler.current(event)
element.addEventListener(eventName, eventListener)
return () => {
element.removeEventListener(eventName, eventListener)
}
}, [eventName, element])
}
// 使用
function KeyTracker() {
const [keys, setKeys] = useState([])
useEventListener('keydown', (event) => {
setKeys(prev => [...prev, event.key])
})
return <div>Pressed keys: {keys.join(', ')}</div>
}
// ========== useIntersectionObserver ==========
function useIntersectionObserver(options = {}) {
const [isIntersecting, setIsIntersecting] = useState(false)
const targetRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting)
}, options)
if (targetRef.current) {
observer.observe(targetRef.current)
}
return () => {
if (targetRef.current) {
observer.unobserve(targetRef.current)
}
}
}, [options])
return [targetRef, isIntersecting]
}
// 使用
function LazyImage({ src, alt }) {
const [ref, isVisible] = useIntersectionObserver({ threshold: 0.1 })
return (
<div ref={ref}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder">Loading...</div>
)}
</div>
)
}六、Hooks 规则与原理
1. Hooks 两条铁律
javascript
// ========== 规则 1: 只能在顶层调用 ==========
// ✗ 错误:在条件语句中调用
function Component({ condition }) {
if (condition) {
const [state, setState] = useState(0) // ❌
}
const [count, setCount] = useState(0) // ✅
}
// ✗ 错误:在循环中调用
function Component({ items }) {
items.forEach(item => {
const [state, setState] = useState(0) // ❌
})
}
// ✗ 错误:在嵌套函数中调用
function Component() {
function handleClick() {
const [state, setState] = useState(0) // ❌
}
}
// ✓ 正确:始终在顶层调用
function Component({ condition, items }) {
const [state1, setState1] = useState(0) // ✅
const [state2, setState2] = useState(0) // ✅
if (condition) {
// 可以使用 state
console.log(state1)
}
}
// ========== 规则 2: 只能在 React 函数中调用 ==========
// ✗ 错误:在普通 JavaScript 函数中调用
function regularFunction() {
const [state, setState] = useState(0) // ❌
}
// ✓ 正确:在 React 函数组件中调用
function MyComponent() {
const [state, setState] = useState(0) // ✅
}
// ✓ 正确:在自定义 Hooks 中调用
function useCustomHook() {
const [state, setState] = useState(0) // ✅
}
// ========== ESLint 插件 ==========
// 安装 eslint-plugin-react-hooks
npm install eslint-plugin-react-hooks --save-dev
// .eslintrc.json
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}2. Hooks 实现原理
javascript
// ========== 简化版 Hooks 实现 ==========
let hooks = []
let currentHookIndex = 0
function useState(initialValue) {
const hookIndex = currentHookIndex
// 首次渲染:初始化
if (hooks[hookIndex] === undefined) {
hooks[hookIndex] = {
state: initialValue,
queue: []
}
}
const hook = hooks[hookIndex]
// setState 函数
const setState = (newValue) => {
const value = typeof newValue === 'function'
? newValue(hook.state)
: newValue
hook.state = value
// 触发重新渲染
render()
}
currentHookIndex++
return [hook.state, setState]
}
function useEffect(callback, deps) {
const hookIndex = currentHookIndex
if (hooks[hookIndex] === undefined) {
hooks[hookIndex] = { deps: undefined, cleanup: undefined }
}
const hook = hooks[hookIndex]
// 检查依赖是否变化
const hasChanged = !deps || !hook.deps ||
deps.some((dep, i) => dep !== hook.deps[i])
if (hasChanged) {
// 执行清理
if (hook.cleanup) {
hook.cleanup()
}
// 执行 effect
hook.cleanup = callback()
hook.deps = deps
}
currentHookIndex++
}
function render(Component) {
// 重置索引
currentHookIndex = 0
// 渲染组件
const element = Component()
// 返回 JSX
return element
}
// 使用示例
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = `Count: ${count}`
}, [count])
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
// 首次渲染
render(Counter)
// hooks = [{ state: 0, queue: [] }, { deps: [0], cleanup: undefined }]
// 点击按钮
setCount(1)
// 重新渲染
render(Counter)
// hooks = [{ state: 1, queue: [] }, { deps: [1], cleanup: undefined }]3. 为什么 Hooks 不能有条件调用?
┌──────────────────────────────────────────────────────────┐
│ Hooks 为什么必须在顶层调用 │
└──────────────────────────────────────────────────────────┘
正确调用顺序:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一次渲染:
function Component() {
useState(0) // Hook 1 → hooks[0]
useEffect(fn) // Hook 2 → hooks[1]
useState('') // Hook 3 → hooks[2]
}
第二次渲染:
function Component() {
useState(0) // 读取 hooks[0] ✓
useEffect(fn) // 读取 hooks[1] ✓
useState('') // 读取 hooks[2] ✓
}
所有 Hook 都能正确对应 ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
错误调用顺序:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一次渲染 (condition = true):
function Component() {
if (true) {
useState(0) // Hook 1 → hooks[0]
}
useState('') // Hook 2 → hooks[1]
}
第二次渲染 (condition = false):
function Component() {
if (false) {
// useState(0) 被跳过
}
useState('') // 读取 hooks[0] ❌
// 期望是字符串,实际是数字!
}
Hook 对应关系错乱 → Bug! ✗
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
解决方案:
✓ 始终在顶层调用
✓ 使用 ESLint 插件检测
✓ 条件逻辑放在 Hook 内部七、常见面试题
题目 1:useState 和 useReducer 的区别?
javascript
// 标准回答要点:
/*
相同点:
- 都用于管理组件状态
- 更新都会触发重渲染
- 都支持函数式更新
区别:
1. 复杂度
- useState: 适合简单状态(布尔值、数字、字符串)
- useReducer: 适合复杂状态(对象、数组、多个子值)
2. 更新逻辑
- useState: 直接设置新值
- useReducer: 通过 dispatch action,由 reducer 纯函数计算新状态
3. 可预测性
- useState: 更新分散在各处
- useReducer: 所有更新逻辑集中在 reducer,易于调试和测试
4. 性能
- useState: 每次更新都创建新的 setter
- useReducer: dispatch 引用稳定,可作为 useCallback 依赖
选择建议:
- 状态独立且简单 → useState
- 状态之间有逻辑关系 → useReducer
- 下一个状态依赖前一个状态 → useReducer
- 需要集中管理更新逻辑 → useReducer
*/
// 示例对比
// useState 适合简单场景
function Toggle() {
const [on, setOn] = useState(false)
return <button onClick={() => setOn(!on)}>{on ? 'ON' : 'OFF'}</button>
}
// useReducer 适合复杂场景
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 })
// 所有更新逻辑集中在 reducer
// ADD_ITEM, REMOVE_ITEM, UPDATE_QUANTITY 等
}题目 2:useEffect 和 useLayoutEffect 的区别?
javascript
// 标准回答要点:
/*
执行时机:
- useEffect: 浏览器绘制后异步执行(不阻塞渲染)
- useLayoutEffect: DOM 更新后、浏览器绘制前同步执行(阻塞渲染)
使用场景:
- useEffect: 大多数副作用(数据获取、订阅、日志)
- useLayoutEffect: 需要同步测量 DOM 或同步更新 DOM
性能影响:
- useEffect: 不会阻塞页面渲染,性能更好
- useLayoutEffect: 会阻塞渲染,可能导致卡顿
选择建议:
- 默认使用 useEffect
- 只有在看到闪烁问题时才改用 useLayoutEffect
*/
// 示例:测量 DOM
// useEffect - 可能闪烁
function MeasureWithEffect() {
const [height, setHeight] = useState(0)
const ref = useRef(null)
useEffect(() => {
// 浏览器已经绘制,用户看到初始高度 0
// 然后更新为实际高度,产生闪烁
setHeight(ref.current.offsetHeight)
}, [])
return <div ref={ref}>Content</div>
}
// useLayoutEffect - 无闪烁
function MeasureWithLayoutEffect() {
const [height, setHeight] = useState(0)
const ref = useRef(null)
useLayoutEffect(() => {
// 浏览器绘制前就更新了,用户看不到中间状态
setHeight(ref.current.offsetHeight)
}, [])
return <div ref={ref}>Content</div>
}题目 3:如何避免 useEffect 无限循环?
javascript
// 标准回答要点:
/*
常见原因:
1. 依赖项在 effect 中被更新
- 解决:移除不必要的依赖或使用函数式更新
2. 对象/数组/函数作为依赖,每次都是新引用
- 解决:使用 useMemo/useCallback 稳定引用
3. 在 effect 中调用 setState 触发重新渲染
- 解决:添加正确的依赖数组或条件判断
预防措施:
- 启用 eslint-plugin-react-hooks
- 仔细审查依赖数组
- 使用 useRef 保存不需要触发更新的值
- 考虑将逻辑提取到自定义 Hooks
*/
// 示例:修复无限循环
// ✗ 无限循环
function BadExample() {
const [data, setData] = useState(null)
useEffect(() => {
fetchData().then(setData)
}, [data]) // data 变化 → effect 执行 → setData → data 变化 → ...
}
// ✓ 修复方案 1:移除依赖
function GoodExample1() {
const [data, setData] = useState(null)
useEffect(() => {
fetchData().then(setData)
}, []) // 仅挂载时执行
}
// ✓ 修复方案 2:使用标志位
function GoodExample2() {
const [data, setData] = useState(null)
const fetched = useRef(false)
useEffect(() => {
if (!fetched.current) {
fetched.current = true
fetchData().then(setData)
}
}, [data])
}题目 4:自定义 Hooks 的优势是什么?
javascript
// 标准回答要点:
/*
优势:
1. 逻辑复用
- 取代 HOC 和 Render Props
- 避免组件树嵌套过深(Wrapper Hell)
2. 关注点分离
- 相关逻辑聚合在一起
- 不像生命周期那样分散
3. 易于测试
- 纯函数,不依赖组件实例
- 可以单独测试 Hook 逻辑
4. 社区生态
- 大量现成的自定义 Hooks
- react-use、ahooks 等库
5. 类型安全
- TypeScript 支持良好
- 完整的类型推断
最佳实践:
- 以 use 开头命名
- 可以组合其他 Hooks
- 返回必要的状态和方法
- 提供清晰的类型定义
*/八、面试标准回答
React Hooks 是 React 16.8 引入的新特性,让函数组件能够使用状态和其他 React 特性,无需编写 class 组件。
核心优势包括:
- 逻辑复用更简单:通过自定义 Hooks 替代 HOC 和 Render Props,避免组件嵌套地狱
- 代码组织更清晰:相关逻辑聚合在一起,而不是分散在生命周期方法中
- 学习曲线更低:无需理解 class、this 绑定等复杂概念
- 更小的打包体积:函数组件比 class 组件更容易压缩和优化
常用 Hooks 分类:
- 状态管理:useState、useReducer
- 副作用处理:useEffect、useLayoutEffect
- 性能优化:useCallback、useMemo、useRef
- 上下文访问:useContext
- 其他:useImperativeHandle、useDebugValue
使用规则有两条铁律:
- 只能在顶层调用,不能在条件、循环或嵌套函数中调用
- 只能在 React 函数组件或自定义 Hooks 中调用
性能优化方面,我会:
- 使用 useCallback 缓存传递给子组件的回调函数
- 使用 useMemo 缓存昂贵的计算结果
- 使用 React.memo 配合稳定的 props 引用
- 避免在 render 中创建新的对象/数组/函数
自定义 Hooks 是逻辑复用的最佳实践,我常用它来封装数据获取、表单处理、本地存储等功能,让组件更专注于 UI 渲染。
常见问题包括闭包陷阱、依赖数组遗漏、无限循环等,通过 ESLint 插件和仔细的代码审查可以避免这些问题。
九、记忆口诀
Hooks 歌诀:
Hooks 让函数变强大,
状态副作用都不怕。
两条规则要牢记,
顶层调用别落下!
useState 管状态,
useEffect 处理副作用。
useCallback 缓存函数,
useMemo 缓存计算结果好!
useRef 存引用,
useContext 跨组件通。
useReducer 复杂态,
自定义 Hooks 逻辑重用!
性能优化三板斧:
memo useCallback useMemo。
依赖数组仔细看,
无限循环要预防!
Hooks 虽好别滥用,
简单场景 useState 够。
复杂逻辑 useReducer,
组合使用最优秀!十、推荐资源
- React 官方 Hooks 文档
- Rules of Hooks
- Building Your Own Hooks
- ahooks - 高质量 React Hooks 库
- react-use - 丰富的 Hooks 集合
- Use Hooks - 交互式学习
十一、总结一句话
- Hooks 核心: 函数组件 + 状态能力 = Class 组件的现代替代 🎯
- 性能优化: useCallback + useMemo + memo = 避免不必要渲染 ⚡
- 逻辑复用: 自定义 Hooks + 组合 = 告别 HOC 嵌套地狱 ✓