组件化与性能优化
本页关键词:props、状态提升、React.memo、useCallback、useMemo、自定义 Hook、re-render 机制
一、组件拆分与 props
React 组件就是函数,通过 props(属性)接收外部数据。
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 类型标注
函数参数使用解构 + 类型标注的写法:
// 冒号左边:从 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 有没有变。
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 renderDisplay 没有被点击,但也 re-render 了——因为父组件 App 重新执行,所有子组件跟着重新执行。
与 Vue 的区别
| Vue | React | |
|---|---|---|
| 更新触发 | 响应式追踪,只更新依赖数据的组件 | 父 re-render → 子全部 re-render |
| DOM 更新 | 精确更新 | 通过 diff 最小化更新 |
| 性能优化 | 默认就比较精确 | 需要手动用 React.memo 等优化 |
React 最终只会把真正变化的部分更新到 DOM(通过 diff),所以页面不会闪烁。但组件函数确实被调用了,存在计算开销。
面试要点:React 默认行为是父组件 re-render 导致所有子组件 re-render。理解这一点是学习性能优化的前提。
三、状态提升
当兄弟组件需要共享同一份数据时,将 state 提升到它们的共同父组件中管理:
App ← state 放在这里
├── Counter ← 接收 count + setCount
└── Display ← 接收 countexport default function App() {
const [count, setCount] = useState(0)
return (
<div>
<Display count={count} />
<Counter count={count} setCount={setCount} />
</div>
)
}判断 state 放在哪里的标准:哪些组件需要这个数据?找到它们的最近公共祖先,state 就放那里。
什么需要定义为 state
这个值变了,界面需要跟着变吗? 需要 → state。不需要 → 普通变量/函数。
todos变了 → 列表要更新 → stateinput变了 → 输入框要更新 → stateaddTodo、removeTodo是操作逻辑 → 普通函数
四、React.memo —— 阻止不必要的 re-render
React.memo 包裹组件后,React 在 re-render 前会先浅比较 props,如果 props 没变就跳过渲染:
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:
const removeTodo = (index: number) => {
setTodos(todos.filter((_, i) => i !== index))
}每次父组件 re-render,这个函数都会被重新创建,虽然逻辑一样,但是新的对象引用:
const a = () => {}
const b = () => {}
a === b // false —— 即使内容完全一样React.memo 用 === 比较 props,发现函数引用变了,照样 re-render。
五、useCallback —— 缓存函数引用
useCallback 缓存一个函数,只有依赖变了才创建新的:
import React, { useState, useCallback } from "react"
const removeTodo = useCallback((index: number) => {
setTodos(prev => prev.filter((_, i) => i !== index))
}, [])两个关键点:
useCallback包裹,依赖数组为[],函数只创建一次,引用保持稳定- 函数式更新
prev => prev.filter(...),不依赖外部的todos变量,避免闭包捕获过期值
React.memo + useCallback 配合使用
// 父组件
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 时,如果 todo、index、removeTodo 都没变,TodoItem 就不会 re-render。
面试要点:
React.memo做浅比较跳过渲染,但如果 props 中有函数,每次 re-render 都会创建新引用导致比较失败。需要配合useCallback稳定函数引用。useCallback内部用函数式更新(prev => ...)避免依赖外部 state。
六、自定义 Hook —— 逻辑复用
自定义 Hook 就是普通函数,内部调用了 React Hook,以 use 开头命名。用于将状态逻辑抽离复用。
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()
// ...
}关键规则
- 必须以
use开头 —— 这是 React 的强制规则,不是建议 - 只能在组件函数或其他 Hook 内部调用 —— 不能在普通函数、if、for 里调用
- 每次调用创建独立的 state —— 多个组件各自调用
useTodos(),state 互不影响
与 Vue 3 Composable 的对比
// 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 | 缓存计算结果 | 昂贵计算避免重复执行 |
优化原则:不要过早优化。先写出正确的代码,发现性能问题时再针对性使用这些工具。