多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE

技术文章

聚焦一线编程技术原理与实战踩坑,沉淀可复用的工程经验。

React Hooks 进阶:useEffect 与性能优化

React Hooks 进阶:useEffect 与性能优化

useEffect 是 React Hooks 中最容易踩坑的 API。它看起来简单——"渲染后执行副作用",但依赖数组、闭包陷阱、重复执行等问题让无数开发者头疼。本文从 useEffect 的执行原理讲起,结合 useMemo/useCallback 与 React.memo,给你一套完整的 React 性能优化方案。

一、useEffect 的执行时机

每次组件渲染完成后,React 都会执行 useEffect 的回调。依赖数组决定了"哪些变化会触发重新执行"。

import { useEffect, useState } from 'react'

function Profile({ userId }) {
  const [user, setUser] = useState(null)

  // 依赖 userId:userId 变化才重新请求
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser)
    // 返回的清理函数会在下次 effect 执行前、组件卸载时调用
    return () => console.log('清理上一次请求', userId)
  }, [userId])

  return <div>{user?.name}</div>
}

依赖数组的三种形态:

  • 不传:每次渲染后都执行(几乎不用)。
  • []:仅挂载后执行一次,相当于 componentDidMount。
  • [dep1, dep2]:依赖变化时执行,最常用。

原则:用到什么外部变量就放进依赖数组。用 ESLint 的 react-hooks 插件帮你补全,不要靠"经验"忽略。

二、闭包陷阱:为什么读到旧值

useEffect 闭包了它执行那次渲染的 state/props。如果依赖数组漏写了某项,effect 里读到的就是"旧渲染"的值,这就是经典的闭包陷阱。

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      // count 始终是 0!因为闭包捕获了首次渲染的 count
      setCount(count + 1)
    }, 1000)
    return () => clearInterval(timer)
  }, []) // ← 漏写 count
  return <div>{count}</div>
}

// 修复方式一:函数式更新,不依赖外部 count
setCount(c => c + 1)

// 修复方式二:把 count 加入依赖(但会让定时器每秒重建)
// useEffect(..., [count])

函数式更新 setCount(c => c + 1) 是最优雅的解法:它接收前一个状态,不依赖闭包里的旧值。

三、useMemo:缓存昂贵计算

每次渲染都会重新执行组件函数体。如果内部有昂贵的计算(如大列表过滤、复杂运算),用 useMemo 缓存,仅在依赖变化时重算。

import { useMemo } from 'react'

function ProductList({ items, keyword }) {
  // 仅当 items 或 keyword 变化时才重新过滤
  const filtered = useMemo(() => {
    return items.filter(it => it.name.includes(keyword))
  }, [items, keyword])

  return (
    <ul>
      {filtered.map(it => <li key={it.id}>{it.name}</li>)}
    </ul>
  )
}

注意:useMemo 本身也有开销,简单计算(如 a + b)无需缓存,否则得不偿失。

四、useCallback:稳定函数引用

每次渲染函数都会被重新创建,引用变化。如果这个函数作为 prop 传给被 React.memo 包裹的子组件,会导致子组件无效重渲染。useCallback 用来稳定函数引用。

import { useCallback, useState } from 'react'

const ExpensiveItem = React.memo(({ onClick, label }) => {
  console.log('ExpensiveItem render')
  return <li onClick={onClick}>{label}</li>
})

function List() {
  const [items, setItems] = useState([])
  // 稳定 handleClick 引用,避免子组件无谓重渲染
  const handleClick = useCallback((id) => {
    setItems(prev => prev.filter(it => it.id !== id))
  }, [])

  return items.map(it => (
    <ExpensiveItem key={it.id} onClick={handleClick} label={it.name} />
  ))
}

五、React.memo:组件级缓存

React.memo 对组件做浅比较,props 不变就跳过渲染。配合 useCallback/useMemo 让 props 引用稳定,是 React 性能优化的标准组合拳。

// 默认浅比较 props
const MemoCard = React.memo(Card)

// 自定义比较函数
const MemoCard2 = React.memo(Card, (prev, next) => {
  return prev.id === next.id && prev.title === next.title
})

六、性能优化的判断原则

性能优化不是越多越好,过度优化反而增加复杂度。给出三条判断标准:

  • 先测量再优化:用 React DevTools Profiler 找到真正慢的组件,别凭感觉加 useMemo。
  • 关注子组件重渲染:列表项、纯展示组件适合 memo;高频更新的状态组件 memo 反而增加比较成本。
  • 状态下沉:把高频变化的状态隔离到小组件,避免父组件重渲染拖累整棵树。
// 状态下沉:搜索框独立持有 keyword,不触发 List 重渲染
function Page({ items }) {
  return (
    <>
      <SearchBar />       {/* keyword 状态在内部 */}
      <List items={items} /> {/* 不受输入影响 */}
    </>
  )
}

React 的性能优化本质上是在回答两个问题:"这次渲染是否必要?"和"这次渲染是否够快?"理解了 useEffect 的依赖机制与渲染流程,你就能精准地回答它们,写出既正确又高效的组件。

返回列表