前言
相信很多小伙伴都遇到过这样的情况:明明只是改了一个小小的状态,整个页面却像中了病毒一样疯狂重渲染,卡得要死要活的。别慌,今天我就带大家深入剖析React的渲染机制,手把手教你写出丝滑流畅的高性能组件!
先来聊聊React的渲染顺序
很多小伙伴可能不知道,React组件的渲染是有严格顺序的:
- 执行阶段:从外到内,像剥洋葱一样一层层往里执行
- 完成阶段:从内到外,像搭积木一样从底层开始完成挂载
这就像盖房子,先搭框架(父组件),再装修细节(子组件),但是验收的时候要从房间开始,最后才是整栋楼。
真正的痛点:无意义的重渲染
来看这个经典的场景:
function App() {
const [count, setCount] = useState(0)
const [num, setNum] = useState(0)
console.log('App render')
return (
<>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>+</button>
<Button num={num}>Click Me</Button>
</>
)
}
当我们点击按钮改变count的时候,按理说Button组件应该淡定如山,毕竟它只关心num啊!但现实很骨感,Button还是会无辜地重新渲染。
这就像你在家里换个灯泡,结果邻居家的狗都跟着汪汪叫,完全没必要啊!
救星登场:React.memo
这时候我们的救星React.memo闪亮登场!它就像给组件加了一层保护罩,只有props真正变化时才会重渲染:
import { useEffect, memo } from 'react'
const Button = ({ num, onClick, children }) => {
useEffect(() => {
console.log('Button UseEffect')
}, [])
console.log('Button render')
return <button onClick={onClick}>{num} {children}</button>
}
// 高阶组件,给Button穿上防护服
export default memo(Button)
有了这层保护,Button组件就能安心做自己的事情,不会被父组件的其他状态变化所打扰。
useMemo:拯救昂贵计算
有时候我们需要做一些复杂的计算,比如:
const expensiveComputation = (n) => {
// 这里模拟一个超级耗时的计算
console.log('expensiveComputation')
for(let i = 0; i < 1000000000; i++) {
i++
}
return n * 2
}
每次组件重渲染都要跑这么重的计算?那还不如回家种田!
这时候useMemo就派上用场了:
const result = useMemo(() => expensiveComputation(num), [num])
这样只有当num真正变化时,才会重新计算。其他时候就像个优雅的贵妇,端着茶杯淡定地说:"不关我事,我不算。"
useCallback:函数也要缓存
还有一个常被忽视的性能杀手:函数的重新创建。
// 错误示范:每次渲染都创建新函数
const handleClick = () => {
console.log('handleClick')
}
// 正确姿势:用useCallback缓存函数
const handleClick = useCallback(() => {
console.log('handleClick')
}, [num])
配合React.memo使用,效果更佳!因为如果每次都传入新的函数引用,memo的浅比较就失效了,又回到了原点。
完整的性能优化实战代码
来看看我们的完整优化方案:
import {
useState,
useEffect,
useCallback,
useMemo
} from 'react'
import './App.css'
import Button from './components/Button'
function App() {
const [count, setCount] = useState(0)
const [num, setNum] = useState(0)
console.log('App render')
const expensiveComputation = (n) => {
// 复杂计算 开销高
console.log('expensiveComputation')
for(let i = 0; i < 1000000000; i++) {
i++
}
return n*2
}
// 缓存计算结果
const result = useMemo(() => expensiveComputation(num), [num])
useEffect(() => {
console.log('count', count)
}, [count])
useEffect(() => {
console.log('num', num)
}, [num])
// 缓存事件处理函数
const handleClick = useCallback(() => {
console.log('handleClick')
}, [num])
return (
<>
<div>{result}</div>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>+</button>
<br />
<button onClick={() => setNum(num + 1)}>+</button>
<br />
<Button num={num} onClick={handleClick}>Click Me Me</Button>
</>
)
}
export default App
组件拆分的艺术
说到性能优化,不得不提组件拆分的粒度问题。
好的拆分策略:
- 单一职责:每个组件只负责一件事
- 状态隔离:相关的状态放在一起,无关的分开
- 复用性考虑:可复用的逻辑抽成独立组件
避免的陷阱:
- 不要把所有状态都塞进一个Context
- 不要过度拆分导致props传递地狱
- 不要为了拆分而拆分
Context使用的坑
很多小伙伴喜欢把所有状态都扔进一个大Context里,觉得这样很方便。但这样做的后果就是:任何一个状态变化,所有使用这个Context的组件都会重渲染!
// 错误示范:把所有鸡蛋放在一个篮子里
const AppContext = createContext({
user: null,
theme: 'light',
todos: [],
cart: [],
// ... 更多状态
})
// 正确做法:按功能拆分Context
const UserContext = createContext()
const ThemeContext = createContext()
const TodoContext = createContext()
这样做的好处是显而易见的:用户信息变化时,只有关心用户信息的组件会重渲染,主题切换不会影响到购物车组件。
总结 🎉
React的性能优化说白了就是减少不必要的重渲染和重复计算。通过合理使用React.memo、useMemo、useCallback这些工具,配合良好的组件设计,我们就能写出既优雅又高性能的React应用。
性能优化是一门艺术,需要在用户体验、开发效率和代码可维护性之间找到最佳平衡点。不要为了那0.1毫秒的提升而让代码变得晦涩难懂。