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

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

相关推荐
妄小闲4 分钟前
网页设计模板 HTML源码网站模板下载
前端·html
icebreaker20 分钟前
tailwindcss 究竟比 unocss 快多少?
前端·css·github
卢叁26 分钟前
Flutter之自定义TabIndicator
前端·flutter
每天吃饭的羊37 分钟前
state和ref
前端·javascript·react.js
GEO_YScsn39 分钟前
Vite:Next-Gen Frontend Tooling 的高效之道——从原理到实践的性能革命
前端·javascript·css·tensorflow
GISer_Jing39 分钟前
滴滴二面(准备二)
前端·javascript·vue·reactjs
ningmengjing_43 分钟前
webpack打包方式
前端·爬虫·webpack·node.js·逆向
摇滚侠44 分钟前
Vue3入门到实战,最新版vue3+TypeScript前端开发教程,笔记03
javascript·笔记·typescript
Yuner20001 小时前
Webpack开发:从入门到精通
前端·webpack·node.js
GISer_Jing1 小时前
滴滴二面准备(一)
前端·javascript·面试·ecmascript