Skip to content

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 使用规则

  1. 只在函数组件或自定义 Hook 中调用
  2. 只在顶层调用(不能在条件、循环、嵌套函数中)
  3. 自定义 Hook 以 use 开头
  4. 依赖数组要诚实:用到的值都要放进去(用 eslint-plugin-react-hooks 检查)

系统学习 Web 前端生态,深入底层架构