React Hooks 心智模型:为什么你总觉得 React 反直觉

React Hooks 难,不是因为 API 多,而是因为你以为是「调用函数」,实际是「注册回调」。这篇讲清这个心智模型,大多数 Hooks 的困惑会自然消解。

一、函数组件不是「调用」,是「描述」

写普通函数时,你调用它,它执行一次返回结果。但函数组件不是这样运行的:

function Counter() {
  const [n, setN] = useState(0)   // 不是「设初始值」,是「注册一个状态」
  console.log('render', n)        // 每次渲染都会打印

  return <button onClick={() => setN(n + 1)}>{n}</button>
}

关键点:这个函数会被反复调用(每次状态变化都调用一次)。所以:

  • 函数体里所有代码在每次渲染时都会重跑(不只是你关心的那几行)
  • useState(0) 里的 只在首次渲染时用一次,后续渲染被忽略
  • 所以 useState(expensive()) 每次都会执行 expensive()(结果被丢弃)——这是性能坑

二、useState:值 + 设置器,不是「属性」

很多人初期的心智模型是「n 是个会变的变量」。其实每次渲染,n 都是一个固定的常量,来自那次渲染的快照。

// 经典陷阱:快速点三次,以为加了 3,实际只加 1
function Bad() {
  const [n, setN] = useState(0)
  return (
    <button onClick={() => {
      setN(n + 1)   // 这里的 n 是「本次渲染的快照」
      setN(n + 1)   // 还是同一个 n,所以第二次覆盖第一次
      setN(n + 1)
    }}>{n}</button>
  )
}

// 正确:用函数式更新,拿到「最新值」
function Good() {
  const [n, setN] = useState(0)
  return (
    <button onClick={() => {
      setN(v => v + 1)
      setN(v => v + 1)
      setN(v => v + 1)   // 会正确累加 3
    }}>{n}</button>
  )
}

为什么函数式更新能正确累加:React 把三次 setN 排进队列,依次用「上一个的最新值」调用回调。v => v + 1 每次拿到的都是队列里前一步的结果,而 n + 1 用的是闭包里的旧快照。

三、useEffect:不是「生命周期」,是「依赖变化后执行」

最常见的错误心智模型是把 useEffect 当成 componentDidMount。它的真实语义是:

useEffect(() => {
  // 这段在「依赖数组里的值发生变化」时执行
  // 首次渲染也算一次「从无到有」的变化
}, [dep1, dep2])

// 三种依赖形态
useEffect(() => {...}, [])        // 只在首次执行(最常见用法)
useEffect(() => {...}, [a])       // a 变了就执行
useEffect(() => {...})            // 不写依赖 = 每次渲染都执行(几乎总是错的)

为什么「不写依赖 = 每次执行」是错的:因为每次渲染都是新的一次,Effect 无条件重跑,里面如果 setState 就会触发下一次渲染,形成死循环。

清理函数的真实作用

// 清理函数在「下次执行前」和「组件卸载时」都会被调用
useEffect(() => {
  const id = setInterval(() => tick(), 1000)
  return () => clearInterval(id)     // 每次重跑前先清上一次的定时器
}, [delay])

// 若不清,delay 变了会累积多个定时器 —— 典型的泄漏

四、闭包陷阱:为什么拿到的是旧值

这是 Hooks 最反直觉的地方:Effect 或回调捕获的是定义那次渲染的变量,即使之后变量变了,闭包里的还是旧值。

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

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count)     // 永远是 0!因为捕获了首次渲染的 count
    }, 1000)
    return () => clearInterval(id)
  }, [])                     // 空依赖 = 只跑一次,闭包永远是最初的
}

// 解法 1:把 count 放进依赖(但会重建定时器)
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000)
  return () => clearInterval(id)
}, [count])

// 解法 2:用函数式更新 / ref 拿最新值
const countRef = useRef(count)
countRef.current = count      // 每次渲染同步
useEffect(() => {
  const id = setInterval(() => console.log(countRef.current), 1000)
  return () => clearInterval(id)
}, [])                        // 不重建定时器,但读到最新值

五、useMemo 与 useCallback:只解决「引用稳定」

这两个常被当成「性能优化万能药」。它们的真实作用只有一个:让依赖比较时的引用保持不变。

// 没有 useCallback:每次渲染都是新函数,子组件的 memo 失效
function Parent() {
  const handler = () => doSomething()   // 每次渲染新引用
  return <Child onClick={handler} />    // Child 每次都会重渲染
}

// 有 useCallback:引用稳定,Child 的 memo 才生效
const handler = useCallback(() => doSomething(), [])
return <Child onClick={handler} />
用法解决什么不解决什么
useMemo昂贵计算重复执行函数引用稳定性(那是 useCallback)
useCallback函数引用每次变化导致子组件重渲染计算开销(那是 useMemo)
两个都不用大多数场景没问题别为了「优化」而加

React 官方建议:先用最简单的写法,测出性能问题再加 memo。滥用 useMemo/useCallback 会让代码难读,收益常常为零——因为 memo 本身也有比较开销。

六、useRef:不触发渲染的可变容器

// 三种常见用途
// 1. DOM 引用
const inputRef = useRef(null)
<input ref={inputRef} />
// inputRef.current.focus()

// 2. 保存「不需要触发渲染」的值
const timerRef = useRef(null)
timerRef.current = setTimeout(...)

// 3. 跨渲染共享最新值(解决闭包陷阱)
const latestRef = useRef(value)
latestRef.current = value
  • ref.current 变了不会触发重渲染——这是它和 state 的核心区别
  • 不要在渲染期间读写 ref 作为状态(会破坏可预测性)
  • useRef 初始值只在首次生效,后续传入被忽略

七、一张表:什么时候用哪个

需求用哪个注意
需要一个会触发重渲染的值useState依赖旧值时用函数式更新
需要副作用(订阅/请求/定时器)useEffect清理函数必须写;依赖要完整
需要稳定的函数引用useCallback只在传给 memo 组件时才有意义
需要缓存昂贵计算useMemo先测性能再加
需要不触发渲染的可变值useRef渲染期间别读写
需要跨组件共享状态Context / 状态库Context 变化会导致所有消费者重渲染

八、总结:三条心智转变

  • 函数组件是「反复执行的描述」,不是「执行一次的函数」——所以函数体的每行都会重跑
  • 每次渲染的变量都是快照——这是闭包陷阱的根源,用函数式更新或 ref 解决
  • Effect 是「依赖变了就执行」,不是生命周期钩子——依赖数组才是它的核心参数
  • useMemo/useCallback 只解决引用稳定,不解决性能问题本身,滥用有害
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇