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

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

相关推荐
汪子熙1 分钟前
浏览器里出现 .angular/cache/19.2.6/abap_test/vite/deps 路径究竟说明了什么
前端·javascript·面试
Benzenene!3 分钟前
让Chrome信任自签名证书
前端·chrome
yangholmes88883 分钟前
如何在 web 应用中使用 GDAL (二)
前端·webassembly
jacy5 分钟前
图片大图预览就该这样做
前端
林太白7 分钟前
Nuxt3 功能篇
前端·javascript·后端
YuJie8 分钟前
webSocket Manager
前端·javascript
Mapmost24 分钟前
Mapmost SDK for UE5 内核升级,三维场景渲染效果飙升!
前端
Mapmost26 分钟前
重磅升级丨Mapmost全面兼容3DTiles 1.1,3DGS量测精度跃升至亚米级!
前端·vue.js·three.js
wycode33 分钟前
Promise(一)极简版demo
前端·javascript