React 组件渲染顺序大揭秘
如果把组件树比作一套嵌套的收纳盒,React 的渲染过程就像整理这些盒子:执行渲染时,会先打开最外层的盒子,再逐层打开里面的小盒子 ------ 也就是从外到内 ,父组件先开始执行,然后递归处理所有子组件;而当所有组件完成挂载、真正在页面上显示时,顺序则反过来,像把盒子一个个合上,先完成最内层小盒子的挂载,再到外层的大盒子,即从内到外。记住这个规律,就能理解很多渲染相关的问题啦~
Button 组件渲染之谜
假设父组件里有个count
状态,还有一个Button
子组件,这Button
既不使用count
,也不依赖它做任何处理。但当你点击按钮让count
增加时,却发现Button
也跟着重新渲染了 ------ 这就像你只是换了双鞋,钱包却非得跟着掏出来检查一遍,纯属多余~
这背后的原因是:函数组件默认具有 "连带反应",只要父组件重新渲染,不管子组件是否用到父组件的状态,都会跟着重新执行。比如父组件里的这段JSX
:
jsx
<Button text="点击提交" />
即使text
始终没变,只要父组件的count
更新,Button
就会 "被迫营业",重新渲染一次。
React 性能优化神器:memo
(一)memo 是什么
React.memo
是个 "精打细算" 的高阶组件,它的核心技能是记忆化(memoization) 。简单说,它会记住组件上次渲染时的props
,如果这次的props
和上次完全一样,就会直接跳过重新渲染,连带着子组件也能 "偷个懒"。对于状态频繁更新的应用来说,这波操作能省不少性能呢~
(二)使用示例
先看没加memo
的情况:
jsx
// 子组件(未使用memo)
const Button = ({ text }) => {
console.log('Button重新渲染了');
return <button>{text}</button>;
};
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>count+1</button>
<Button text="固定文本" />
</div>
);
};
每次点击 "count+1",Button
都会打印 "重新渲染",明明text
没变化,这就是典型的无效渲染。
加上memo
后:
jsx
// 子组件(使用memo)
const Button = memo(({ text }) => {
console.log('Button重新渲染了');
return <button>{text}</button>;
});
这时再点击 "count+1",Button
就会 "坚守岗位",只有text
真的变了才会重新渲染~
(三)自定义比较函数
memo
默认只会对props
做浅层比较 (比如基本类型值是否相等,引用类型只比较地址)。如果props
里有对象或数组,可能会 "误判"。这时候可以自定义比较规则:
jsx
const UserCard = memo(
({ user }) => <div>{user.name}</div>,
// 自定义比较:只要name没变,就不重新渲染
(prevProps, nextProps) => prevProps.user.name === nextProps.user.name
);
这样就算user
对象地址变了,只要name
相同,组件也能稳住不渲染~
组件划分粒度的艺术
(一)组件拆分原则
组件拆分就像切披萨,不是越大越难啃,也不是越小越麻烦。核心原则是单向数据流 :父组件通过props
传递数据,子组件只负责渲染,不直接修改props
。
比如一个商品列表,拆成ProductList
(管理数据)和ProductItem
(渲染单条商品),既方便复用,又便于维护 ------ 就像把披萨切成小块,吃起来才香嘛~
(二)状态更新与组件函数
当组件状态更新时,整个函数会重新执行一遍。如果组件里有复杂计算,就像每次出门都要把所有衣服翻出来再叠回去,特别耗时。
这时候除了memo
,useCallback
和useMemo
也能派上大用场。
(三)useCallback 和 React.memo 搭配使用
useCallback
的作用是 "保鲜" 函数。为啥需要它?因为每次组件重新执行,内部定义的函数都会变成新的(地址变了)。就算子组件用了memo
,发现函数props
的地址变了,还是会重新渲染 ------ 等于白优化了!
看个例子:
jsx
// 父组件(未用useCallback)
const Parent = () => {
const [count, setCount] = useState(0);
// 每次Parent渲染,handleClick都是新函数
const handleClick = () => console.log('点击了');
return <Child onClick={handleClick} />;
};
// 子组件(用了memo)
const Child = memo(({ onClick }) => {
console.log('Child重新渲染了');
return <button onClick={onClick}>点我</button>;
});
当count
变化时,handleClick
地址变了,Child
会跟着渲染。这时候给handleClick
套上useCallback
:
jsx
const handleClick = useCallback(() => {
console.log('点击了');
}, []); // 依赖为空:永远返回同一个函数
这样handleClick
地址不变,Child
就能安心不渲染了~ 这对组合简直是性能优化黄金搭档!
综合案例分析
下面结合完整代码,看看这些优化手段怎么配合使用:
App.js(父组件)
jsx
import { useState, useEffect, useCallback, useMemo } from 'react';
import Button from './Button';
function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
console.log('App render');
// 复杂计算:用useMemo缓存结果
const expensiveCalculation = (n) => {
console.log('执行复杂计算');
// 模拟耗时操作
for (let i = 0; i < n * 10000000; i++) {}
return n + 10;
};
// 只有num变了,才重新计算
const result = useMemo(() => expensiveCalculation(num), [num]);
// 用useCallback缓存函数:依赖num,num不变则函数不变
const handleBtnClick = useCallback(() => {
console.log('按钮被点击');
}, [num]);
return (
<div>
<p>计算结果:{result}</p>
{/* 改变count(不影响Button) */}
<button onClick={() => setCount(count + 1)}>count+1</button>
<br />
{/* 改变num(影响Button的num props) */}
<button onClick={() => setNum(num + 1)}>num+1</button>
<br />
<Button num={num} onClick={handleBtnClick}>
点击我
</Button>
</div>
);
}
export default App;
Button.js(子组件)
jsx
import { useEffect, memo } from 'react';
// 用memo包裹:只有props真的变了才渲染
const Button = memo(({ num, onClick, children }) => {
useEffect(() => {
console.log('Button挂载完成');
}, []); // 空依赖:只执行一次
console.log('Button render');
return <button onClick={onClick}>{num} {children}</button>;
});
export default Button;
总结:性能优化三板斧
-
理解渲染顺序:执行外到内,挂载内到外,避免因顺序问题导致的逻辑 bug。
-
善用 memo :给纯展示组件套上
memo
,阻止无关更新。 -
搭配 useCallback/useMemo :缓存函数和计算结果,让
memo
的优化不白费。
记住:性能优化不是炫技,而是让用户体验更丝滑的手段。按需使用,才能恰到好处~