Skip to content

组件化与性能优化

本页关键词:props、状态提升、React.memo、useCallback、useMemo、自定义 Hook、re-render 机制


一、组件拆分与 props

React 组件就是函数,通过 props(属性)接收外部数据。

tsx
function Counter({ count, setCount }: {
  count: number
  setCount: React.Dispatch<React.SetStateAction<number>>
}) {
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

export default function App() {
  const [count, setCount] = useState(0)
  return <Counter count={count} setCount={setCount} />
}

props 的 TypeScript 类型标注

函数参数使用解构 + 类型标注的写法:

tsx
// 冒号左边:从 props 对象中解构
// 冒号右边:TypeScript 类型定义
function TodoInput({ input, setInput, addTodo }: {
  input: string
  setInput: (value: string) => void
  addTodo: () => void
}) { ... }
  • input: string —— 字符串类型
  • setInput: (value: string) => void —— 函数类型,接收 string 参数,无返回值
  • addTodo: () => void —— 无参无返回值的函数

这是 TypeScript 语法,不是 React 的。用 .jsx 则不需要写类型部分。

面试要点:React 的数据流是单向的(父 → 子),子组件通过 props 接收数据,通过回调函数通知父组件修改。


二、re-render 机制

核心规则

父组件 re-render → 所有子组件都会 re-render,不管子组件的 props 有没有变。

tsx
function Display({ count }: { count: number }) {
  console.log("Display render")
  return <p>当前计数:{count}</p>
}

function Counter({ count, setCount }: {
  count: number
  setCount: React.Dispatch<React.SetStateAction<number>>
}) {
  console.log("Counter render")
  return <button onClick={() => setCount(count + 1)}>+1</button>
}

export default function App() {
  const [count, setCount] = useState(0)
  console.log("App render")

  return (
    <div>
      <Display count={count} />
      <Counter count={count} setCount={setCount} />
    </div>
  )
}

点击按钮后,控制台输出:

App render
Display render
Counter render

Display 没有被点击,但也 re-render 了——因为父组件 App 重新执行,所有子组件跟着重新执行。

与 Vue 的区别

VueReact
更新触发响应式追踪,只更新依赖数据的组件父 re-render → 子全部 re-render
DOM 更新精确更新通过 diff 最小化更新
性能优化默认就比较精确需要手动用 React.memo 等优化

React 最终只会把真正变化的部分更新到 DOM(通过 diff),所以页面不会闪烁。但组件函数确实被调用了,存在计算开销。

面试要点:React 默认行为是父组件 re-render 导致所有子组件 re-render。理解这一点是学习性能优化的前提。


三、状态提升

当兄弟组件需要共享同一份数据时,将 state 提升到它们的共同父组件中管理:

App          ← state 放在这里
├── Counter  ← 接收 count + setCount
└── Display  ← 接收 count
tsx
export default function App() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <Display count={count} />
      <Counter count={count} setCount={setCount} />
    </div>
  )
}

判断 state 放在哪里的标准:哪些组件需要这个数据?找到它们的最近公共祖先,state 就放那里。

什么需要定义为 state

这个值变了,界面需要跟着变吗? 需要 → state。不需要 → 普通变量/函数。

  • todos 变了 → 列表要更新 → state
  • input 变了 → 输入框要更新 → state
  • addTodoremoveTodo 是操作逻辑 → 普通函数

四、React.memo —— 阻止不必要的 re-render

React.memo 包裹组件后,React 在 re-render 前会先浅比较 props,如果 props 没变就跳过渲染:

tsx
const TodoItem = React.memo(function TodoItem({ todo, index, removeTodo }: {
  todo: string
  index: number
  removeTodo: (index: number) => void
}) {
  console.log("TodoItem render", index)
  return (
    <li onClick={() => removeTodo(index)}>
      {todo}(点击删除)
    </li>
  )
})

陷阱:函数引用不稳定

用了 React.memo 后,如果传入的 props 中包含函数,可能仍然会 re-render:

tsx
const removeTodo = (index: number) => {
  setTodos(todos.filter((_, i) => i !== index))
}

每次父组件 re-render,这个函数都会被重新创建,虽然逻辑一样,但是新的对象引用:

javascript
const a = () => {}
const b = () => {}
a === b  // false —— 即使内容完全一样

React.memo=== 比较 props,发现函数引用变了,照样 re-render。


五、useCallback —— 缓存函数引用

useCallback 缓存一个函数,只有依赖变了才创建新的:

tsx
import React, { useState, useCallback } from "react"

const removeTodo = useCallback((index: number) => {
  setTodos(prev => prev.filter((_, i) => i !== index))
}, [])

两个关键点:

  1. useCallback 包裹,依赖数组为 [],函数只创建一次,引用保持稳定
  2. 函数式更新 prev => prev.filter(...),不依赖外部的 todos 变量,避免闭包捕获过期值

React.memo + useCallback 配合使用

tsx
// 父组件
const removeTodo = useCallback((index: number) => {
  setTodos(prev => prev.filter((_, i) => i !== index))
}, [])

// 子组件用 React.memo 包裹
const TodoItem = React.memo(function TodoItem({ todo, index, removeTodo }: {
  todo: string
  index: number
  removeTodo: (index: number) => void
}) {
  return <li onClick={() => removeTodo(index)}>{todo}</li>
})

这样在父组件 re-render 时,如果 todoindexremoveTodo 都没变,TodoItem 就不会 re-render。

面试要点React.memo 做浅比较跳过渲染,但如果 props 中有函数,每次 re-render 都会创建新引用导致比较失败。需要配合 useCallback 稳定函数引用。useCallback 内部用函数式更新(prev => ...)避免依赖外部 state。


六、自定义 Hook —— 逻辑复用

自定义 Hook 就是普通函数,内部调用了 React Hook,以 use 开头命名。用于将状态逻辑抽离复用。

tsx
function useTodos() {
  const [todos, setTodos] = useState<string[]>([])
  const [input, setInput] = useState("")

  const addTodo = () => {
    if (input.trim() === "") return
    setTodos([...todos, input])
    setInput("")
  }

  const removeTodo = useCallback((index: number) => {
    setTodos(prev => prev.filter((_, i) => i !== index))
  }, [])

  return { todos, input, setInput, addTodo, removeTodo }
}

// 使用
export default function App() {
  const { todos, input, setInput, addTodo, removeTodo } = useTodos()
  // ...
}

关键规则

  1. 必须以 use 开头 —— 这是 React 的强制规则,不是建议
  2. 只能在组件函数或其他 Hook 内部调用 —— 不能在普通函数、if、for 里调用
  3. 每次调用创建独立的 state —— 多个组件各自调用 useTodos(),state 互不影响

与 Vue 3 Composable 的对比

javascript
// Vue 3 Composable
export function useTodos() {
  const todos = ref([])
  const addTodo = (text) => { todos.value.push(text) }
  return { todos, addTodo }
}

// React 自定义 Hook
function useTodos() {
  const [todos, setTodos] = useState([])
  const addTodo = (text) => { setTodos([...todos, text]) }
  return { todos, addTodo }
}

思想完全一致——逻辑复用。区别在于 Vue 用 ref 直接修改,React 用 setter 函数创建新值。

面试要点:自定义 Hook 是 React 逻辑复用的核心方式,等价于 Vue 3 的 Composable。Hook 的两个约束:以 use 开头、只能在组件或 Hook 顶层调用。为什么叫"钩子"?因为它"钩住"了 React 的内部能力(state、生命周期等),普通函数做不到。


七、性能优化总结

工具作用使用场景
React.memo浅比较 props,跳过不必要的 re-render子组件 props 不常变化时
useCallback缓存函数引用函数作为 props 传给 memo 组件时
useMemo缓存计算结果昂贵计算避免重复执行

优化原则:不要过早优化。先写出正确的代码,发现性能问题时再针对性使用这些工具。