大家好,我是小杨。作为一名摸爬滚打了近6年的前端老司机,今天想和大家聊聊React中那个最基础却又最容易让人困惑的问题------到底什么时候会触发重新渲染?
记得刚接触React时,我总以为setState()一定会让界面变化,结果被现实狠狠上了一课。直到在真实项目中踩了无数坑后才明白,React的渲染机制远比表面看起来的精妙。
一、先破除一个经典误区
刚开始我写过这样的代码:
jsx
function MyComponent() {
let count = 0;
const handleClick = () => {
count += 1;
console.log('Count updated:', count); // 数值变了但界面没更新!
};
return (
<div>
<p>Current count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
看到问题了吗?直接修改局部变量不会触发渲染。这就是第一个重要知识点:只有通过特定的状态更新机制才会触发重新渲染。
二、真正触发渲染的三大核心场景
1. 状态改变(setState/useState)
这是我最初学会的正确方式:
jsx
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prevCount => prevCount + 1); // 这才是正确的触发方式
};
return (
<div>
<p>Current count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
但这里有个坑:即使设置相同的值,默认也会触发渲染。直到后来项目遇到性能问题,我才学会用优化:
jsx
const [user, setUser] = useState({ id: 1, name: 'John' });
// 只有在新值不同时才触发渲染
setUser(prevUser => {
return prevUser.id === newUser.id ? prevUser : newUser;
});
2. Props变化
在我参与的一个电商项目中,商品列表组件是这样工作的:
jsx
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<ProductItem key={product.id} product={product} />
))}
</div>
);
}
// 当父组件传入新的products数组时,ProductList就会重新渲染
3. 父组件重新渲染
这是最容易忽略的一点。有一次我调试了半天性能问题,才发现是这个原因:
jsx
function Parent() {
const [value, setValue] = useState('');
return (
<div>
<input value={value} onChange={e => setValue(e.target.value)} />
{/* 每次输入都会导致Child重新渲染! */}
<Child />
</div>
);
}
function Child() {
console.log('Child rendered'); // 每次输入都会打印
return <div>Static Content</div>;
}
三、高级场景:那些不太明显的渲染触发点
Context变化 consumers自动更新
在开发主题切换功能时,我这样实现:
jsx
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Content />
</ThemeContext.Provider>
);
}
function Header() {
// 当theme变化时,只有实际使用theme的组件会重新渲染
const { theme } = useContext(ThemeContext);
return <header className={theme}>Header</header>;
}
useReducer的dispatch
在状态逻辑复杂的项目中,我更喜欢用useReducer:
jsx
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload];
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = text => {
dispatch({ type: 'ADD_TODO', payload: text }); // 触发渲染
};
}
四、实战中的性能优化技巧
在大型项目中,不必要的渲染会严重影响性能。这是我积累的一些实战经验:
1. React.memo 避免重复渲染
jsx
const ExpensiveComponent = React.memo(function({ data }) {
// 只在props变化时重新渲染
return <div>{/* 复杂计算 */}</div>;
});
2. useMemo & useCallback
jsx
function UserList({ users, onUserSelect }) {
const filteredUsers = useMemo(() => {
return users.filter(user => user.active); // 缓存计算结果
}, [users]);
const handleSelect = useCallback((userId) => {
onUserSelect(userId); // 缓存函数引用
}, [onUserSelect]);
return (
<div>
{filteredUsers.map(user => (
<UserItem key={user.id} user={user} onSelect={handleSelect} />
))}
</div>
);
}
3. 关键渲染日志调试法
这是我自创的调试技巧,在复杂组件中添加:
jsx
function ComplexComponent(props) {
// 渲染日志
useEffect(() => {
console.log('Component rendered with props:', props);
});
return /* ... */;
}
五、从虚拟DOM到实际渲染的完整流程
让我用一次完整的更新流程来总结:
- 触发更新:setState()被调用
- 调度渲染:React将更新加入队列
- 协调阶段:比较虚拟DOM差异
- 提交阶段:将差异应用到真实DOM
- 绘制阶段:浏览器实际绘制页面
jsx
function UpdateFlowExample() {
const [value, setValue] = useState('');
// 1. 事件处理
const handleChange = (e) => {
setValue(e.target.value); // 触发更新
};
// 3. 渲染阶段
return (
<div>
<input value={value} onChange={handleChange} />
<DisplayValue value={value} />
</div>
);
}
// 4. 只有value变化时才会重新渲染
const DisplayValue = React.memo(({ value }) => {
return <p>Current value: {value}</p>;
});
最后说两句
理解了React的渲染机制,就像拿到了性能优化的钥匙。还记得我在第一个大型React项目中,因为不懂这些原理,页面卡得让人怀疑人生。现在回头看,那些踩过的坑都成了最宝贵的经验。
希望我的分享能帮你少走弯路。如果有问题,欢迎在评论区交流------毕竟6年了,我还在不断学习React的新技巧呢!
⭐ 写在最后
请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.
✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式
✅ 认为我部分代码过于老旧,可以提供新的API或最新语法
✅ 对于文章中部分内容不理解
✅ 解答我文章中一些疑问
✅ 认为某些交互,功能需要优化,发现BUG
✅ 想要添加新功能,对于整体的设计,外观有更好的建议
✅ 一起探讨技术加qq交流群:906392632
最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!