React 渲染 是将组件状态转换为用户界面的过程。这不是简单的 DOM 操作,而是一个声明式的、可预测的状态到 UI 的转换。
react渲染主要有两个阶段,render和commit
render阶段是React通过Diff算法计算出哪些部分需要更新,并打上标签(effectTag),同时构建一棵新的Fiber树(workInProgress树)。这个过程是异步的,可以被中断。React会根据任务的优先级来调度任务,高优先级的任务可以打断低优先级的任务。在这个阶段,React会执行组件的渲染(调用render方法或函数组件本身)并得到React元素(虚拟DOM),然后与旧的Fiber树进行比较,生成新的Fiber树。这个过程是纯JS计算,不会产生任何副作用(如DOM更新),因此可以被中断和恢复。
commit阶段将Render阶段计算出的变更应用到真实的DOM上。这个过程是同步的,不可中断的。因为一旦开始更新DOM,就必须连续完成,否则可能会导致UI不一致。在这个阶段,React会执行生命周期方法(如componentDidMount、componentDidUpdate)和Hook(如useLayoutEffect)等,然后更新DOM。之后,React会执行一些副作用清理和调度后续的副作用(如useEffect)
React 渲染生命周期
触发更新 → 渲染阶段 → 提交阶段 → 绘制阶段
↓ ↓ ↓ ↓
状态变化 计算差异 更新DOM 浏览器渲染
触发渲染的 4 种情况
// 1. 初始渲染
ReactDOM.createRoot(root).render(<App />);
// 2. 状态更新(常用)
const [count, setCount] = useState(0);
setCount(1); // 触发重新渲染
// 3. 父组件渲染(默认行为)
function Parent() {
return <Child />; // Parent渲染时,Child也会渲染
}
// 4. Context 变化
const value = useContext(MyContext); // Context更新时,消费者重新渲染
渲染阶段 (Render) --创建虚拟 DOM
虚拟 DOM
// 虚拟dom并不是真正的DOM,而是轻量级JavaScript对象
const virtualDOMElement = {
type: 'div',
props: {
className: 'container',
children: [
{
type: 'h1',
props: { children: 'Hello' }
}
]
}
};
// React元素实际上是这样的对象
const element = <div className="container"><h1>Hello</h1></div>;
协调(Reconciliation)
// React如何比较新旧虚拟DOM?
function reconcile(oldTree, newTree) {
// 1. 类型不同 → 完全替换
if (oldTree.type !== newTree.type) {
return { action: 'REPLACE', node: newTree };
}
// 2. 类型相同 → 更新属性
const propChanges = diffProps(oldTree.props, newTree.props);
// 3. 递归比较子节点
const childUpdates = reconcileChildren(oldTree.children, newTree.children);
return { action: 'UPDATE', propChanges, childUpdates };
}
// 关键优化:Diffing策略
// - 同层比较(不会跨层级移动组件)
// - key属性帮助识别元素(列表渲染必备)
key
// 使用索引作为key,如果是渲染列表,会导致多次回流,导致页面的性能大幅下降
{items.map((item, index) => (
<ListItem key={index} item={item} />
// 问题:重新排序时,React会混淆组件实例
))}
// 使用唯一ID
{items.map(item => (
<ListItem key={item.id} item={item} />
// React可以正确识别元素,复用DOM节点
))}
// 没有key时,React会警告,性能下降
// key就是最大的作用就是为了减少对dom的操作
提交阶段(Commit)--更新真实 DOM
React 18+ 的自动批处理
// React 17及以前:只在React事件处理中批处理
function handleClick() {
setCount(c => c + 1); // 触发重新渲染
setFlag(f => !f); // 触发重新渲染
// 结果:2次渲染(在React 17中)
}
// React 18:自动批处理所有更新
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// 结果:1次渲染(合并批处理)
}
// 强制同步刷新(特殊情况)
flushSync(() => {
setCount(c => c + 1); // 立即渲染
});
浏览器渲染时机
// React更新DOM后,浏览器还需要做这些事:
1. 样式计算(Recalculate Style)
2. 布局(Layout/Reflow)
3. 绘制(Paint)
4. 合成(Composite)
// React尽量最小化触发布局抖动
function BadExample() {
const [width, setWidth] = useState(100);
useEffect(() => {
// 连续读取和写入DOM,导致布局抖动
const element = document.getElementById('box');
const currentWidth = element.offsetWidth; // 读取(强制同步布局)
element.style.width = `${currentWidth + 10}px`; // 写入(触发布局)
}, []);
return <div id="box" style={{ width }}>Box</div>;
}
组件渲染优化
React.memo - 组件缓存
// 普通组件:父组件渲染就会重新渲染
const ExpensiveComponent = ({ data }) => {
console.log('渲染了!'); // 频繁打印
return <div>{data}</div>;
};
// 使用React.memo:浅比较props
const OptimizedComponent = React.memo(({ data }) => {
console.log('只有data变化时才渲染');
return <div>{data}</div>;
});
// 自定义比较函数(深度比较不推荐)
const DeepEqualComponent = React.memo(
ExpensiveComponent,
(prevProps, nextProps) => {
// 返回true表示"跳过渲染"
return _.isEqual(prevProps, nextProps);
}
);
useMemo & useCallback - 值/函数缓存
function Parent({ items }) {
const [count, setCount] = useState(0);
// 每次渲染都创建新函数
const handleClick = () => console.log('clicked');
// 缓存函数(依赖项变化时才重新创建)
const memoizedHandleClick = useCallback(() => {
console.log('clicked');
}, []); // 空数组 = 只创建一次
// 每次渲染都重新计算
const expensiveValue = items.filter(item => item.active).sort();
// 缓存计算结果
const memoizedValue = useMemo(() => {
return items.filter(item => item.active).sort();
}, [items]); // items变化时才重新计算
return <Child onClick={memoizedHandleClick} data={memoizedValue} />;
}
状态提升 vs 状态下沉
// 状态提升:共享状态放在最近的共同祖先
function App() {
const [user, setUser] = useState(null);
return (
<Layout>
<Header user={user} />
<Content user={user} />
<Sidebar user={user} /> {/* 不必要的重新渲染 */}
</Layout>
);
}
// 状态下沉:状态放在需要的地方
function App() {
return (
<Layout>
<Header /> {/* Header自己管理user状态 */}
<Content />
<Sidebar />
</Layout>
);
}
// Context优化:分割Context
const UserContext = createContext();
const SettingsContext = createContext();
// 避免单个大Context导致所有消费者重新渲染
并发渲染(React 18+)
特性
// 传统渲染:同步、不可中断
function oldRender() {
// 如果组件树很大,会阻塞主线程
renderComponentTree();
// 用户交互会卡顿
}
// 并发渲染:可中断、优先级调度
function concurrentRender() {
// React把渲染工作分成小任务
// 可以暂停渲染来处理高优先级更新
// 然后继续之前的工作
}
// 使用并发特性
import { startTransition, useDeferredValue } from 'react';
function SearchBox() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // 延迟更新
// 紧急更新:用户输入
function handleChange(e) {
setQuery(e.target.value); // 立即更新输入框
}
// 非紧急更新:搜索结果
function handleSearch(newQuery) {
startTransition(() => {
setQuery(newQuery); // 标记为非紧急更新
});
}
return (
<>
<input value={query} onChange={handleChange} />
<SearchResults query={deferredQuery} /> {/* 使用延迟值 */}
</>
);
}
性能分析工具
React DevTools
// 1. 组件树检查
// 2. Profiler 录制分析
// 3. 高亮更新组件
// Chrome DevTools 中的使用步骤:
// 1. 打开 Profiler 标签
// 2. 点击录制
// 3. 执行用户交互
// 4. 停止录制,分析火焰图
实际性能检查清单
// 渲染时自问这些问题:
function Component() {
// 1. 这个渲染是必要的吗?
// 2. 计算是否过于昂贵?
// 3. 子组件是否会不必要地重新渲染?
// 4. 是否有布局抖动?
// 5. 是否可以使用并发特性优化?
return <div>...</div>;
}
常见问题
1.不必要的对象创建
// 每次渲染都创建新对象
function Component({ style }) {
return <div style={{ color: 'red', ...style }} />;
}
// 使用useMemo或提取常量
const defaultStyle = { color: 'red' };
function Component({ style }) {
const mergedStyle = useMemo(() => ({
...defaultStyle,
...style
}), [style]);
return <div style={mergedStyle} />;
}
2.依赖项数组问题
// 忘记依赖项
useEffect(() => {
fetchData(id);
}, []); // id变化时不会重新获取,只会初始渲染一次
// 依赖项过多导致频繁运行
useEffect(() => {
// 每次props变化都运行
}, [props]); // props是一个对象,每次都是新的引用
// 精确指定依赖项
useEffect(() => {
fetchData(id);
}, [id]); // 只有id变化时才运行
//场景
const [miscellaneous, setMiscellaneous] = useState([])
useEffect(() => {
const fetchMiscellaneous = async () => {
try {
const res = await useMiscellaneous.getMiscellaneousList()
const data = res.reverse()
setMiscellaneous(data)