React】解决react执行两遍的问题

React】解决React执行两遍的问题

一、背景与问题

在React开发中,开发者常会遇到组件渲染逻辑执行两遍的诡异现象。这种问题在开发环境尤为明显,特别是在使用React的热更新(Hot Module Replacement, HMR)功能时,开发服务器会主动重新渲染组件以反映代码变更。然而,这种行为在某些场景下可能引发性能问题,甚至导致状态管理异常。

典型场景包括:

  1. 列表组件中使用不稳定的key属性导致重复渲染
  2. 状态更新触发的副作用函数执行两次
  3. 使用useEffect时未正确处理依赖项变更
  4. 非受控组件在输入事件中触发的重复渲染

这种问题在生产环境可能不明显,但在开发阶段频繁触发时,会显著影响开发效率。

二、基本原理

React的双渲染机制源于其核心的协调算法(Reconciliation)。在开发模式下,React会执行两次渲染:

  1. 检查渲染:计算组件的虚拟DOM,但不实际更新DOM
  2. 实际渲染:根据差异更新DOM,避免不必要的重排重绘

这种机制虽然保证了开发时的即时反馈,但会导致某些副作用函数(如useEffect)被触发两次。关键在于理解React的渲染生命周期和副作用的执行规则。

三、环境准备

我们使用Create React App创建基础项目,确保版本兼容性:

npx create-react-app react-double-render
cd react-double-render
npm install

项目结构建议:

src/
├── components/        # 通用组件
├── hooks/            # 自定义Hook
├── utils/            # 工具函数
├── App.js            # 主组件
└── index.js          # 入口文件

四、核心实现

1. 基础问题演示

// App.js
import React, { useState, useEffect } from 'react';

function App() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('Effect triggered', count);
  }, [count]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>Increment</button>
    </div>
  );
}

export default App;

在开发模式下,点击按钮时会看到两次日志输出,这是React的双渲染机制导致的。

2. 解决方案:使用useRef缓存副作用

// hooks/useCustomEffect.js
import { useEffect, useRef } from 'react';

export function useCustomEffect(callback, dependencies) {
  const isMounted = useRef(true);
  
  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);
  
  useEffect(() => {
    if (isMounted.current) {
      callback();
    }
  }, dependencies);
}
// App.js
import React, { useState } from 'react';
import { useCustomEffect } from './hooks/useCustomEffect';

function App() {
  const [count, setCount] = useState(0);
  
  useCustomEffect(() => {
    console.log('Custom effect triggered', count);
  }, [count]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>Increment</button>
    </div>
  );
}

export default App;

关键点解释:

  • useRef创建的isMounted变量在组件卸载时置为false
  • 在副作用执行时检查isMounted状态,避免执行已卸载的副作用
  • 这种模式特别适用于需要处理副作用的函数组件

3. 使用React.memo优化子组件

// components/Counter.js
import React from 'react';

function Counter({ value }) {
  console.log('Rendering Counter', value);
  return (
    <div>Value: {value}</div>
  );
}

export default React.memo(Counter);
// App.js
import React, { useState } from 'react';
import Counter from './components/Counter';

function App() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>Increment</button>
      <Counter value={count} />
    </div>
  );
}

export default App;

关键点:

  • React.memo对子组件进行浅比较
  • 只有props变化时才会触发重新渲染
  • 适用于性能敏感的子组件优化

五、完整案例

构建一个待办事项管理器,展示不同组件的渲染行为:

// App.js
import React, { useState, useEffect } from 'react';
import TodoList from './components/TodoList';
import AddTodoForm from './components/AddTodoForm';

function App() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');

  useEffect(() => {
    console.log('App component rendered');
  }, []);

  const addTodo = () => {
    if (newTodo.trim()) {
      setTodos([...todos, { id: Date.now(), text: newTodo }]);
      setNewTodo('');
    }
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Todo List</h1>
      <AddTodoForm 
        newTodo={newTodo} 
        setNewTodo={setNewTodo} 
        addTodo={addTodo} 
      />
      <TodoList todos={todos} />
    </div>
  );
}

export default App;
// components/AddTodoForm.js
import React from 'react';

function AddTodoForm({ newTodo, setNewTodo, addTodo }) {
  console.log('Rendering AddTodoForm');
  
  return (
    <div>
      <input 
        value={newTodo} 
        onChange={(e) => setNewTodo(e.target.value)} 
        placeholder="Enter new todo"
      />
      <button onClick={addTodo}>Add</button>
    </div>
  );
}

export default React.memo(AddTodoForm);
// components/TodoList.js
import React from 'react';

function TodoList({ todos }) {
  console.log('Rendering TodoList');
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

export default React.memo(TodoList);

在开发模式下,每次添加新待办事项时,会看到多次日志输出。通过使用React.memo优化子组件,可以减少不必要的渲染次数。

六、源码解析

React的双渲染机制核心在于ReactDOM.render和ReactDOM.hydrate的实现。在开发模式中,React会执行两次渲染:

  1. 首次渲染:创建虚拟DOM并进行差异比较
  2. 更新渲染:根据差异更新DOM
// React源码片段(简化版)
function render(element, container, callback) {
  const prevChildren = container._children;
  const nextChildren = element;
  
  // 第一次渲染
  const firstRender = renderChildren(prevChildren, nextChildren, container);
  
  // 第二次渲染
  const secondRender = renderChildren(prevChildren, nextChildren, container);
  
  // 执行DOM更新
  updateDOM(firstRender, secondRender, container);
}

七、进阶使用

  1. 使用useCallback优化子组件

    const handleAdd = useCallback(() => {
      // ...
    }, [todos]);
  2. 使用useMemo缓存计算结果

    const filteredTodos = useMemo(() => {
      return todos.filter(todo => todo.text.includes('test'));
    }, [todos]);
  3. 使用useRef处理副作用

    const ref = useRef(null);
    
    useEffect(() => {
      ref.current = () => {
        // ...
      };
    }, []);
  4. 使用React.lazy和Suspense实现代码分割

    const LazyComponent = React.lazy(() => import('./LazyComponent'));
    
    function App() {
      return (
        <React.Suspense fallback="Loading...">
          <LazyComponent />
        </React.Suspense>
      );
    }

八、性能与工程实践

1. 性能优化方法

  • 使用React.memo和PureComponent优化子组件
  • 使用useMemo和useCallback避免重复计算
  • 使用shouldComponentUpdate进行手动优化
  • 使用React.lazy和Suspense实现代码分割

2. 安全风险分析

  • 不正确的状态管理可能导致XSS攻击
  • 未验证的用户输入可能导致注入攻击
  • 未处理的异常可能导致组件崩溃

3. 异常处理策略

useEffect(() => {
  try {
    // 可能抛出异常的代码
  } catch (error) {
    console.error('Caught error in effect:', error);
  }
}, [dependencies]);

九、常见问题与踩坑

1. 常见错误

错误示例:

useEffect(() => {
  console.log('Effect triggered', count);
}, [count]);

问题分析: 在开发模式下,这个副作用会被触发两次,导致日志输出两次。

解决办法: 使用useRef缓存副作用或使用useCustomEffect自定义钩子。

2. 使用场景分析

应该使用:

  • 需要处理副作用的函数组件
  • 需要优化子组件渲染性能
  • 需要处理复杂的依赖关系

不应该使用:

  • 简单的函数组件
  • 无需处理副作用的场景
  • 需要立即执行的初始化逻辑

十、最佳实践

  1. 使用React.memo优化性能敏感的子组件
  2. 使用useCallback和useMemo避免不必要的计算
  3. 在useEffect中使用try-catch处理异常
  4. 使用key属性正确管理列表组件
  5. 在开发模式下使用React Developer Tools分析渲染次数

十一、总结

React的双渲染机制是其开发模式下的核心特性,虽然可能带来性能开销,但通过合理使用React.memo、useCallback、useMemo等工具,可以有效控制渲染行为。在实际开发中,需要根据具体场景选择合适的优化策略,既要避免不必要的重复渲染,又要保证代码的可维护性和可读性。通过深入理解React的渲染机制,开发者能够更高效地构建高性能的React应用。

最后修改于:2026年09月16日 15:36

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日