React的渲染时机:聊透虚拟DOM的更新机制

大家好,我是小杨。作为一名摸爬滚打了近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到实际渲染的完整流程

让我用一次完整的更新流程来总结:

  1. 触发更新:setState()被调用
  2. 调度渲染:React将更新加入队列
  3. 协调阶段:比较虚拟DOM差异
  4. 提交阶段:将差异应用到真实DOM
  5. 绘制阶段:浏览器实际绘制页面
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

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

相关推荐
Z兽兽4 小时前
React@18+Vite项目配置env文件
前端·react.js·前端框架
SuniaWang4 小时前
《Spring AI + 大模型全栈实战》学习手册系列 · 专题六:《Vue3 前端开发实战:打造企业级 RAG 问答界面》
java·前端·人工智能·spring boot·后端·spring·架构
A_nanda5 小时前
根据AI提示排查vue前端项目
前端·javascript·vue.js
happymaker06265 小时前
web前端学习日记——DAY05(定位、浮动、视频音频播放)
前端·学习·音视频
~无忧花开~6 小时前
React状态管理完全指南
开发语言·前端·javascript·react.js·前端框架
LegendNoTitle6 小时前
计算机三级等级考试 网络技术 选择题考点详细梳理
服务器·前端·经验分享·笔记·php
@大迁世界6 小时前
1.什么是 ReactJS?
前端·javascript·react.js·前端框架·ecmascript
BJ-Giser7 小时前
Cesium 基于EZ-Tree的植被效果
前端·可视化·cesium
王码码20358 小时前
Flutter for OpenHarmony:Flutter 三方库 algoliasearch 毫秒级云端搜索体验(云原生搜索引擎)
android·前端·git·flutter·搜索引擎·云原生·harmonyos
发现一只大呆瓜8 小时前
深入浅出 AST:解密 Vite、Babel编译的底层“黑盒”
前端·面试·vite