React 组件渲染

React 渲染机制与重新渲染原理

本文档简单解析 React 的渲染机制、重新渲染原理以及性能优化策略,帮助开发者更好地理解 React 的工作原理并编写高性能的应用。

React 渲染机制概述

在 React 中,渲染(Render) 是指将组件转换为 DOM 节点的过程。这个过程包括:

  1. 组件函数执行 :调用组件函数,生成虚拟 DOM ;
  2. 虚拟 DOM 比较Diff算法 比较新旧 虚拟DOM 树 ;
  3. DOM 更新 :将差异应用到真实 DOM ;

渲染的两个阶段

渲染过程主要分为两个阶段:

1. Render 阶段(渲染阶段)
  • 目的 :计算状态和 props 的变化,生成新的 虚拟 DOM ;
  • 特点 :可以被打断,React 可以 暂停、中止或重启 这个阶段 ;
  • 操作 :执行组件函数,进行 Diff 比较 ;
2. Commit 阶段(提交阶段)
  • 目的:将变化应用到真实 DOM
  • 特点不能被中断,必须同步执行
  • 操作:更新 DOM 节点,执行副作用(useEffect)
javascript 复制代码
// 渲染过程示例
const Component = ({ name }) => {
  // Render 阶段:执行组件函数
  const [count, setCount] = useState(0);
  
  // Render 阶段:生成虚拟 DOM
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

// Commit 阶段:更新真实 DOM,执行副作用
useEffect(() => {
  console.log('Component mounted or updated');
}, [count]);

React 渲染原理

React 使用虚拟 DOM 来提高性能, 虚拟 DOM 实际上就是一个 JSON 对象:

javascript 复制代码
// 虚拟 DOM 结构示例
const virtualDOM = {
  type: 'div',
  props: {
    className: 'container',
    children: [
      {
        type: 'h1',
        props: {
          children: 'Hello World'
        }
      },
      {
        type: 'button',
        props: {
          onClick: handleClick,
          children: 'Click me'
        }
      }
    ]
  }
};

Diff 算法

React 使用 Diff 算法来比较新旧虚拟 DOM 树:

1. 同层比较

React 只比较同一层级的节点,不会跨层级比较:

javascript 复制代码
// 旧树
<div>
  <ComponentA />
  <ComponentB />
</div>

// 新树
<div>
  <ComponentA />
  <ComponentC />  {/* 只比较这一层 */}
</div>
2. Key 的作用

Key 帮助 React 识别哪些元素发生了变化:

javascript 复制代码
// 没有 key:React 会重新创建所有元素
{items.map(item => <Item data={item} />)}

// 有 key:React 可以复用相同 key 的元素
{items.map(item => <Item key={item.id} data={item} />)}
3. 组件类型比较

React 会比较组件的类型:

javascript 复制代码
// 组件类型相同:复用组件实例
<Button onClick={handleClick} />

// 组件类型不同:卸载旧组件,挂载新组件
<Button onClick={handleClick} />  // 旧
<Link href="/path" />             // 新

渲染优先级

React 18 引入了并发特性,支持不同优先级的渲染:

javascript 复制代码
// 高优先级更新(用户交互)
const handleClick = () => {
  setCount(count + 1); // 立即渲染
};

// 低优先级更新(数据获取)
const fetchData = async () => {
  const data = await api.getData();
  setData(data); // 可能被延迟渲染
};

组件重新渲染触发条件

理解 React 组件重新渲染的触发条件是进行性能优化的基础。以下是导致组件重新渲染的主要情况:

1. 组件内部状态改变

当组件内部使用 useStateuseReducer 等 Hook 管理的状态发生变化时,组件会重新渲染。

javascript 复制代码
const Component = () => {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1); // 状态改变,触发重新渲染
  };

  console.log('render');

  return <button onClick={handleClick}>Count: {count}</button>;
};

2. 父组件重新渲染

当父组件重新渲染时,默认情况下所有子组件都会重新渲染,无论子组件是否有 props 变化。

javascript 复制代码
const Parent = () => {
  const [parentState, setParentState] = useState(0);

  console.log('Parent render');
  return (
    <div>
      <button onClick={() => setParentState(parentState + 1)}>
        Parent: {parentState}
      </button>
      <Child /> {/* 父组件重新渲染时,Child 也会重新渲染 */}
    </div>
  );
};

const Child = () => {
  console.log('Child render'); // 每次父组件渲染都会执行
  return <div>Child Component</div>;
};

3. Props 改变

当父组件传递给子组件的 props 发生变化时,子组件会重新渲染。

  • 示例1
javascript 复制代码
// 示例1:通过外部数据源改变 props
const Parent = () => {
  const [externalData, setExternalData] = useState({ name: 'Alice' });
  
  // 模拟外部数据变化(比如从 API 获取)
  const handleDataChange = () => {
    setExternalData({ name: externalData.name === 'Alice' ? 'Bob' : 'Alice' });
  };

  console.log('Parent render');
  return (
    <div>
      <button onClick={handleDataChange}>Change External Data</button>
      <Child user={externalData} /> {/* externalData 改变时,Child 重新渲染 */}
    </div>
  );
};

const Child = ({ user }) => {
  console.log('Child render,user:', user.name);
  return <div>Hello, {user.name}!</div>;
};
  • 示例2
javascript 复制代码
// 示例2:通过计算属性改变 props
const Parent = () => {
  const [count, setCount] = useState(0);
  
  // 计算属性作为 props 传递给子组件
  const computedValue = count * 2;

  console.log('Parent render');
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child value={computedValue} /> {/* computedValue 改变时,Child 重新渲染 */}
    </div>
  );
};

const Child = ({ value }) => {
  console.log('Child render,value:', value);
  return <div>Value: {value}</div>;
};

这里需要区分两个概念:

  1. Props 改变导致重新渲染:当传递给子组件的 props 发生变化时,子组件会重新渲染
  2. 父组件重新渲染导致子组件重新渲染:当父组件重新渲染时,默认情况下所有子组件都会重新渲染,无论 props 是否变化

关键点:在 React 中,子组件的重新渲染主要有两个原因:

  • 父组件重新渲染(这是默认行为)
  • Props 发生变化(这也会触发重新渲染)

要阻止不必要的重新渲染,需要使用 React.memo 来包装子组件,这样只有当 props 真正发生变化时,子组件才会重新渲染。

javascript 复制代码
// 示例:展示父组件重新渲染对子组件的影响
const Parent = () => {
  const [parentState, setParentState] = useState(0);
  const [childProp, setChildProp] = useState('initial');
  
  console.log('Parent render');
  return (
    <div>
      <button onClick={() => setParentState(parentState + 1)}>
        Change Parent State: {parentState}
      </button>
      <button onClick={() => setChildProp(childProp === 'initial' ? 'changed' : 'initial')}>
        Change Child Prop: {childProp}
      </button>
      
      {/* 即使 childProp 没有变化,Child 也会因为父组件重新渲染而重新渲染 */}
      <Child prop={childProp} />
      
      {/* 使用 React.memo 包装的组件,只有 props 变化时才重新渲染 */}
      <MemoizedChild prop={childProp} />
    </div>
  );
};

const Child = ({ prop }) => {
  console.log('Child render,prop:', prop);
  return <div>Child: {prop}</div>;
};

const MemoizedChild = React.memo(({ prop }) => {
  console.log('MemoizedChild render,prop:', prop);
  return <div>MemoizedChild: {prop}</div>;
});

在这个示例中:

  • 点击"Change Parent State"按钮时,Child 会重新渲染(因为父组件重新渲染),但 MemoizedChild 不会重新渲染(因为 props 没有变化)
  • 点击"Change Child Prop"按钮时,两个子组件都会重新渲染(因为 props 发生了变化)

4. Context 值改变

当组件消费的 Context 值发生变化时,所有消费该 Context 的组件都会重新渲染。

javascript 复制代码
const ThemeContext = createContext();

const App = () => {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
      <ThemedComponent /> {/* theme 改变时重新渲染 */}
    </ThemeContext.Provider>
  );
};

const ThemedComponent = () => {
  const theme = useContext(ThemeContext);
  return <div className={theme}>Themed Content</div>;
};

5. 强制重新渲染

通过 forceUpdate(类组件)或自定义 Hook 强制触发重新渲染。

javascript 复制代码
// 类组件中的强制更新
class Component extends React.Component {
  handleForceUpdate = () => {
    this.forceUpdate(); // 强制重新渲染
  };
}

// 函数组件中的强制更新(不推荐)
const Component = () => {
  const [, forceUpdate] = useReducer(x => x + 1, 0);
  
  const handleForceUpdate = () => {
    forceUpdate(); // 强制重新渲染
  };
};

6. 组件卸载和重新挂载

当组件的 key 发生变化时,React 会卸载旧组件并挂载新组件,这也会触发重新渲染。

javascript 复制代码
const Parent = () => {
  const [key, setKey] = useState(0);
  
  console.log('render');
  return (
    <div>
      <button onClick={() => setKey(key + 1)}>Remount</button>
      <Child key={key} /> {/* key 改变时,Child 会卸载并重新挂载 */}
    </div>
  );
};

重新渲染的传播机制

React 的重新渲染遵循向下传播 的原则:当 App 中的 state 改变时,整个组件树都会重新渲染。

javascript 复制代码
const App = () => {
  const [state, setState] = useState(0);
  
  console.log('App render');
  return (
    <div>
      <button onClick={() => setState(state + 1)}>Update</button>
      <Level1 /> {/* 会重新渲染 */}
    </div>
  );
};

const Level1 = () => {
  console.log('Level-1 渲染');
  return <Level2 />; // 会重新渲染
};

const Level2 = () => {
  console.log('Level-2 渲染');
  return <Level3 />; // 会重新渲染
};

const Level3 = () => {
  console.log('Level-3 渲染');
  return <div>Final Level</div>;
};

中断重新渲染链

可以使用 React.memo 来中断重新渲染链:只有 App 组件会重新渲染,子组件不受影响。

javascript 复制代码
const App = () => {
  const [state, setState] = useState(0);
  
  console.log('App render');
  return (
    <div>
      <button onClick={() => setState(state + 1)}>Update</button>
      <MemoizedLevel1 /> {/* 使用 React.memo 包装 */}
    </div>
  );
};

const MemoizedLevel1 = React.memo(() => {
  console.log('Level-1 渲染');
  return <Level2 />;
});

const Level2 = React.memo(() => {
  console.log('Level-2 渲染');
  return <Level3 />;
});

const Level3 = React.memo(() => {
  console.log('Level-3 渲染');
  return <div>Final Level</div>;
});

性能优化建议

  1. 合理使用 React.memo :对于纯展示组件使用 React.memo
  2. 状态下沉:将状态移动到合适的组件层级
  3. 避免不必要的状态提升:不要将状态提升得太高
  4. 使用 useMemo 和 useCallback:在必要时记忆化计算结果和函数
  5. 拆分 Context:避免 Context 值频繁变化导致大量组件重新渲染

理解这些重新渲染的触发条件对于编写高性能的 React 应用非常重要。


渲染性能优化策略

1. 组件拆分策略

将大组件拆分为小组件,减少重新渲染的范围:

javascript 复制代码
// ❌ 大组件:任何状态改变都会重新渲染整个组件
const LargeComponent = () => {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);
  
  return (
    <div>
      <UserProfile user={user} />
      <PostList posts={posts} />
      <CommentList comments={comments} />
    </div>
  );
};

// ✅ 拆分组件:只有相关部分会重新渲染
const OptimizedComponent = () => {
  return (
    <div>
      <UserSection />
      <PostSection />
      <CommentSection />
    </div>
  );
};

const UserSection = () => {
  const [user, setUser] = useState(null);
  return <UserProfile user={user} />;
};

const PostSection = () => {
  const [posts, setPosts] = useState([]);
  return <PostList posts={posts} />;
};

const CommentSection = () => {
  const [comments, setComments] = useState([]);
  return <CommentList comments={comments} />;
};

2. 状态提升与下沉

合理管理状态的位置,避免不必要的重新渲染:

javascript 复制代码
// ❌ 状态提升过高
const App = () => {
  const [modalOpen, setModalOpen] = useState(false);
  
  return (
    <div>
      <Header />
      <MainContent />
      <Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
    </div>
  );
};

// ✅ 状态下沉到合适位置
const App = () => {
  return (
    <div>
      <Header />
      <MainContent />
    </div>
  );
};

const MainContent = () => {
  const [modalOpen, setModalOpen] = useState(false);
  
  return (
    <div>
      <Content />
      <Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
    </div>
  );
};

3. 记忆化优化

使用 React.memouseMemouseCallback 进行记忆化:

javascript 复制代码
// 使用 React.memo 包装纯组件
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      processed: true
    }));
  }, [data]);
  
  const handleUpdate = useCallback((id) => {
    onUpdate(id);
  }, [onUpdate]);
  
  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} data={item} onUpdate={handleUpdate} />
      ))}
    </div>
  );
});

4. 懒加载组件

使用 React.lazySuspense 进行代码分割:

javascript 复制代码
import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

const App = () => {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
};

5. Context 优化

合理设计 Context 结构,避免不必要的重新渲染:

javascript 复制代码
// ❌ 混合状态和 API
const AppContext = createContext();

const AppProvider = ({ children }) => {
  const [state, setState] = useState(initialState);
  
  const value = {
    state,
    updateState: setState,
    // 其他 API
  };
  
  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
};

// ✅ 分离状态和 API
const StateContext = createContext();
const APIContext = createContext();

const AppProvider = ({ children }) => {
  const [state, setState] = useState(initialState);
  
  const api = useMemo(() => ({
    updateState: setState,
    // 其他 API
  }), []);
  
  return (
    <APIContext.Provider value={api}>
      <StateContext.Provider value={state}>
        {children}
      </StateContext.Provider>
    </APIContext.Provider>
  );
};

6. 渲染性能监控

使用性能监控工具来识别性能瓶颈:

javascript 复制代码
import { Profiler } from 'react';

const onRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
  console.log('Component:', id);
  console.log('Phase:', phase);
  console.log('Actual Duration:', actualDuration);
  console.log('Base Duration:', baseDuration);
};

const App = () => {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <YourComponent />
    </Profiler>
  );
};

7. 避免在渲染中创建对象

避免在渲染过程中创建新的对象或函数:

javascript 复制代码
// ❌ 每次渲染都创建新对象
const Component = ({ items }) => {
  const style = { color: 'red', fontSize: '16px' };
  const handleClick = () => console.log('clicked');
  
  return (
    <div style={style} onClick={handleClick}>
      {items.map(item => <Item key={item.id} data={item} />)}
    </div>
  );
};

// ✅ 使用 useMemo 和 useCallback
const Component = ({ items }) => {
  const style = useMemo(() => ({ color: 'red', fontSize: '16px' }), []);
  const handleClick = useCallback(() => console.log('clicked'), []);
  
  return (
    <div style={style} onClick={handleClick}>
      {items.map(item => <Item key={item.id} data={item} />)}
    </div>
  );
};
相关推荐
sjd_积跬步至千里2 小时前
CSS实现文字横向无限滚动效果
前端
维他AD钙2 小时前
前端基础避坑:3 个实用知识点的简单用法
前端
journs2 小时前
micro-app微前端styled-components CSSOM模式 应用切换样式丢失问题
前端
呼啦啦小魔仙2 小时前
elpis项目DSL设计分享
前端
李李记2 小时前
别让 “断字” 毁了 Canvas 界面!splitByGrapheme 轻松搞定非拉丁文本换行
前端·canvas
来金德瑞2 小时前
快速掌握 ProseMirror 的核心概念
前端
ygria2 小时前
样式工程化:如何实现Design System
前端·前端框架·前端工程化
墨渊君3 小时前
“蒙”出花样!用 CSS Mask 实现丝滑视觉魔法
前端·css
huabuyu4 小时前
基于 React + MarkdownIt 的 Markdown 渲染器实践:支持地图标签和长按复制
前端