React 函数组件与类组件:渲染快照有何不同?

React 函数组件与类组件:渲染快照有何不同?

React 的函数组件与类组件都能描述界面,但它们最值得理解的差异不是语法,而是一次渲染看到的值如何被保存。函数组件中的事件处理器会捕获创建它的那次渲染快照;类组件的方法通常通过可变的 this 读取最新 props 和 state。

一个延迟提示的例子

函数组件:

function Profile({ user }: { user: string }) {
  function handleClick() {
    window.setTimeout(() => {
      window.alert(`Followed ${user}`)
    }, 3000)
  }

  return <button onClick={handleClick}>Follow</button>
}

点击按钮时 user 是 “Dan”,随后父组件切换成 “Sophie”。三秒后弹窗仍显示 “Dan”,因为这次 handleClick 来自显示 Dan 的那次渲染。

类组件:

class Profile extends React.Component<{
  user: string
}> {
  handleClick = () => {
    window.setTimeout(() => {
      window.alert(
        `Followed ${this.props.user}`
      )
    }, 3000)
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Follow
      </button>
    )
  }
}

定时器执行时才读取 this.props.user。React 已经把实例上的 props 更新为 Sophie,因此它通常显示 Sophie。

这不是“函数异步、类同步”,也不是闭包本身有问题。差异在于:

  • 函数组件读取的是某次渲染中的局部常量。
  • 类方法读取的是同一个组件实例上后来被替换的属性。

React state 是一次渲染的快照

在函数组件的一次渲染中,props、state 和由它们计算出的局部变量都不会因为调用 setState 而在当前函数里改变:

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

  function handleClick() {
    setCount(count + 1)
    console.log(count) // 仍是本次渲染中的 count
  }

  return <button onClick={handleClick}>{count}</button>
}

状态更新会请求下一次渲染。下一次渲染创建新的 count 和新的事件处理器;旧处理器仍保留旧快照。React 官方把这个模型称为 state as a snapshot

需要基于前一个状态连续更新时,使用函数式更新:

setCount((current) => current + 1)

想在类组件里保留点击时的值

类组件也能显式创建快照:

handleClick = () => {
  const { user } = this.props

  window.setTimeout(() => {
    window.alert(`Followed ${user}`)
  }, 3000)
}

关键不是组件类型,而是读取发生在点击时还是回调执行时。

函数组件什么时候需要最新值

有时需求确实是让延迟回调读取最新值,例如 WebSocket 回调始终使用最新筛选条件。可以用 ref 保存“可变的最新值”:

function useLatest<T>(value: T) {
  const ref = useRef(value)
  ref.current = value
  return ref
}

function Search({ query }: { query: string }) {
  const latestQuery = useLatest(query)

  useEffect(() => {
    const unsubscribe = subscribe(() => {
      console.log(latestQuery.current)
    })

    return unsubscribe
  }, [latestQuery])

  return null
}

ref 是有意逃离快照模型的工具。不要为了绕过 Effect 依赖而把所有值都塞进 ref;那会让数据流重新变得隐式。

Effect 与闭包

Effect 也捕获所属渲染的值。依赖数组告诉 React 何时用新闭包重新同步外部系统:

useEffect(() => {
  const connection = connect(roomId)
  return () => connection.disconnect()
}, [roomId])

遗漏 roomId 会让 Effect 继续使用旧房间。解决办法通常是补齐依赖、调整数据流或拆分 Effect,而不是关闭 lint 规则。

现在应该用哪一种

React 仍支持类组件,但官方建议新代码使用函数组件。函数组件与 Hooks 更适合复用状态逻辑,也让“每次渲染的输入”更明确。现有稳定类组件不必为了风格统一全部重写;迁移应有测试和业务收益。

错误边界仍是常见的类组件用例。其他场景通常优先函数组件,再根据框架和组件库约束决定。

参考:

理解“渲染快照”和“可变实例”之后,很多所谓闭包陷阱都会变成一个更具体的问题:这个异步回调应该看到触发时的值,还是执行时的最新值?

最后更新于

ihopeful Blog 由博主亲笔撰写,重要信息可放心引用。