React 渲染机制与重新渲染原理
本文档简单解析 React 的渲染机制、重新渲染原理以及性能优化策略,帮助开发者更好地理解 React 的工作原理并编写高性能的应用。
React 渲染机制概述
在 React 中,渲染(Render) 是指将组件转换为 DOM 节点的过程。这个过程包括:
- 组件函数执行 :调用组件函数,生成
虚拟 DOM
; - 虚拟 DOM 比较 :
Diff算法
比较新旧虚拟DOM 树
; - 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. 组件内部状态改变
当组件内部使用 useState
、useReducer
等 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>;
};
这里需要区分两个概念:
- Props 改变导致重新渲染:当传递给子组件的 props 发生变化时,子组件会重新渲染
- 父组件重新渲染导致子组件重新渲染:当父组件重新渲染时,默认情况下所有子组件都会重新渲染,无论 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>;
});
性能优化建议
- 合理使用 React.memo :对于纯展示组件使用
React.memo
- 状态下沉:将状态移动到合适的组件层级
- 避免不必要的状态提升:不要将状态提升得太高
- 使用 useMemo 和 useCallback:在必要时记忆化计算结果和函数
- 拆分 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.memo
、useMemo
和 useCallback
进行记忆化:
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.lazy
和 Suspense
进行代码分割:
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>
);
};