1 React更新机制概述
React的渲染流程可以简化为:JSX → 虚拟DOM → 真实DOM。当组件的props或state发生改变时,React会触发更新流程:重新执行render函数,产生新的虚拟DOM树,然后通过Diff算法对比新旧虚拟DOM树的差异,最后将差异更新到真实DOM上。在这个过程中,React会采用高效的同层比较算法(时间复杂度为O(n)),但即使如此,不必要的render函数执行仍然会造成计算资源的浪费。
值得注意的是,在React的默认行为中,只要父组件重新渲染,其所有子组件都会重新渲染,即使子组件的props和state没有任何变化。这种"过度渲染"在大中型应用中会导致明显的性能问题,因为虚拟DOM的生成和对比需要消耗大量的计算资源。为了解决这个问题,React提供了多种性能优化手段,包括shouldComponentUpdate、PureComponent和React.memo,它们都基于一个简单的原则:只有当组件依赖的数据真正发生变化时,才执行渲染过程。
2 SCU原理与源码分析
2.1 SCU的作用与默认行为
shouldComponentUpdate(简称SCU)是React提供的生命周期方法,它允许开发者控制组件是否需要重新渲染。SCU在组件更新流程中被调用,接收新的props、新的state和新的context作为参数,并返回一个布尔值来决定是否继续渲染流程。
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState, nextContext) {
// 通过比较当前和未来的props/state决定是否更新
if (this.props.someValue !== nextProps.someValue) {
return true; // 只有someValue变化时才重新渲染
}
return false;
}
render() {
return <div>{this.props.someValue}</div>;
}
}
在没有自定义SCU方法的情况下,React组件的默认行为总是返回true,即任何时候props、state或context发生变化,组件都会重新渲染。这种保守策略确保了渲染结果的正确性,但牺牲了性能。
2.2 源码中的SCU调用机制
在React源码中,SCU的调用发生在协调过程(reconciliation) 中。具体来说,在ReactFiberClassComponent.new.js文件的checkShouldComponentUpdate函数中包含了相关逻辑:
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext
) {
const instance = workInProgress.stateNode;
// 如果组件实现了shouldComponentUpdate方法,则使用它的返回值
if (typeof instance.shouldComponentUpdate === 'function') {
const shouldUpdate = instance.shouldComponentUpdate(
newProps, newState, nextContext
);
return shouldUpdate;
}
// 如果是PureComponent,使用浅比较逻辑
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) ||
!shallowEqual(oldState, newState)
);
}
// 默认返回true
return true;
}
从源码可以看出,React优先检查用户自定义的shouldComponentUpdate实现。如果存在,就使用它的返回值;如果没有,则检查是否是PureComponent,如果是则进行浅层比较;如果都不是,则默认返回true。
2.3 SCU的注意事项
使用SCU时需要考虑一些重要事项。首先,不正确实现SCU可能导致bug,例如,如果只比较了部分props或state,而忽略了其他相关数据,组件可能不会在需要时更新。其次,SCU中的比较逻辑应该是快速且高效的,复杂的比较操作可能比直接渲染更消耗性能。最后,当使用深层嵌套对象时,SCU的实现会变得复杂,因为需要深度比较所有相关属性。
3 PureComponent原理与源码解析
3.1 PureComponent的继承结构
PureComponent是React提供的内置优化组件,它与普通Component的唯一区别是多了一个isPureReactComponent静态属性。在React源码的ReactBaseClasses.js中,可以看到PureComponent的实现:
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// 将Component.prototype上的方法合并到PureComponent中
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
从代码可以看出,PureComponent通过原型继承的方式继承了Component的功能,然后通过Object.assign将Component原型上的方法合并到自己的原型上,最后设置isPureReactComponent标记为true。这种实现方式确保了PureComponent拥有Component的全部功能,同时增加了浅比较优化的能力。
3.2 浅比较(shallowEqual)机制
PureComponent的核心在于浅比较算法,当组件的props或state发生变化时,React会对新旧props和state进行浅层比较。浅比较的实现位于shallowEqual.js文件中:
function shallowEqual(objA, objB) {
// 首先检查两个对象是否相同(包括+0和-0、NaN的特殊处理)
if (Object.is(objA, objB)) {
return true;
}
// 如果其中一个不是对象或为null,直接返回false
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// 比较键的数量
if (keysA.length !== keysB.length) {
return false;
}
// 比较每个键的值
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!Object.is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
浅比较算法首先使用Object.is比较两个值是否相同(这种方法能够正确处理NaN、+0和-0的情况),然后检查两个对象键的数量是否相同,最后逐个比较每个键对应的值。这种比较是浅层的,意味着它只比较对象的第一层属性,不会递归比较嵌套对象。
3.3 PureComponent的使用示例
使用PureComponent可以显著减少不必要的渲染,提升性能:
class MyList extends React.PureComponent {
render() {
return (
<ul>
{this.props.items.map(item =>
<li key={item.id}>{item.text}</li>
)}
</ul>
);
}
}
class App extends React.Component {
state = {
items: [{id: 1, text: 'Item 1'}, {id: 2, text: 'Item 2'}]
};
addItem = () => {
// 错误方式:直接修改state
// this.state.items.push({id: 3, text: 'Item 3'});
// this.setState({items: this.state.items});
// 正确方式:创建新数组
this.setState(prevState => ({
items: [...prevState.items, {id: 3, text: 'Item 3'}]
}));
};
render() {
return (
<div>
<button onClick={this.addItem}>Add Item</button>
<MyList items={this.state.items} />
</div>
);
}
}
上面的示例演示了正确使用PureComponent的方法。由于PureComponent使用浅比较,必须确保传递给它的props是不可变数据。如果直接修改数组或对象而不是创建新副本,浅比较将无法检测到变化,导致组件不更新。
4 memo原理与源码解析
4.1 memo的工作机制
React.memo是一个高阶组件,用于优化函数组件的渲染性能。它与PureComponent类似,但针对函数组件而非类组件。memo通过记忆化技术缓存组件渲染结果,只有在props发生变化时才重新渲染组件。
memo的基本用法如下:
const MyComponent = React.memo(function MyComponent(props) {
// 只在props发生变化时重新渲染
return <div>{props.value}</div>;
});
// 或者带有自定义比较函数
const MyComponent = React.memo(
function MyComponent(props) {
return <div>{props.value}</div>;
},
function areEqual(prevProps, nextProps) {
// 返回true表示不重新渲染,false表示需要重新渲染
return prevProps.value === nextProps.value;
}
);
memo接受两个参数:要包装的组件和可选的比较函数。如果没有提供比较函数,memo会使用浅比较来比较props,这与PureComponent的行为一致。
4.2 源码中的memo实现
在React源码中,memo的实现主要位于ReactMemo.js和ReactFiberBeginWork.new.js中。memo组件的类型标记是MemoComponent,在更新时会调用updateMemoComponent函数:
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
updateLanes: Lanes,
renderLanes: Lanes,
): null | Fiber {
if (current !== null) {
// 检查是否有调度更新或上下文更改
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current, renderLanes
);
if (!hasScheduledUpdateOrContext) {
const prevProps = current.memoizedProps;
// 默认使用浅比较,或者使用用户自定义的比较函数
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) &&
current.ref === workInProgress.ref) {
// 如果props没有变化,则跳过渲染
return bailoutOnAlreadyFinishedWork(
current, workInProgress, renderLanes
);
}
}
}
// 否则继续正常渲染流程
// ...
}
从源码可以看出,updateMemoComponent函数会检查组件是否有已调度的更新或上下文更改。如果没有,它会获取memo的compare方法(未自定义则取默认的shallowEqual),然后比较新旧props的变化。如果比较结果为true(即props没有变化),则调用bailoutOnAlreadyFinishedWork来阻止组件的重新渲染。
4.3 useMemo与useCallback的协同使用
要充分发挥memo的优化效果,通常需要与useMemo和useCallback Hook配合使用。这些Hook可以帮助避免引用类型属性(如对象、数组、函数)的不必要变化,从而避免memo优化失效。
const ExpensiveComponent = React.memo(({ items, onClick }) => {
// 渲染逻辑
});
function ParentComponent() {
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
// 使用useCallback缓存函数,避免每次渲染都创建新函数
const handleClick = useCallback(() => {
console.log('Item clicked');
}, []); // 依赖数组为空,函数只创建一次
// 使用useMemo缓存计算结果,避免每次渲染都重新计算
const processedItems = useMemo(() => {
return items.map(item => ({
...item,
computed: someExpensiveComputation(item)
}));
}, [items]); // 只有当items变化时重新计算
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveComponent
items={processedItems}
onClick={handleClick}
/>
</div>
);
}
useCallback和useMemo的实现原理类似,都基于依赖数组的比较。在ReactFiberHooks.js中,updateCallback和updateMemo函数都会通过areHookInputsEqual函数来比较依赖数组的各项是否发生变化:
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (nextDeps !== null) {
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
areHookInputsEqual函数会使用Object.is来逐个比较依赖项的当前值和之前值。只有当所有依赖项都没有变化时,useCallback和useMemo才会返回缓存的值而不是新计算的值。
5 性能优化实践与注意事项
5.1 最佳实践与优化策略
在实际项目中有效使用SCU、PureComponent和memo需要遵循一些最佳实践:
- 优先使用函数组件与memo:在新项目中,优先使用函数组件结合React.memo进行优化,这与React的未来发展方向一致。
- 合理选择优化位置:不需要对所有组件进行优化,优先优化大型列表、复杂组件和深层嵌套组件,这些地方最容易产生性能问题。
- 不可变数据模式:坚持使用不可变数据模式,避免直接修改对象或数组,而是创建新副本。可以使用扩展运算符、Object.assign或不可变数据库(如Immer)来简化操作。
// 不可变数据更新示例
const [state, setState] = useState({
items: [],
config: { /* 大型配置对象 */ }
});
// 更新状态的正确方式 - 创建新对象
setState(prevState => ({
...prevState,
items: [...prevState.items, newItem]
}));
// 深层更新的正确方式
setState(prevState => ({
...prevState,
config: {
...prevState.config,
setting: newValue
}
}));
5.2 潜在陷阱与解决方案
尽管这些优化手段能提升性能,但如果使用不当,可能会带来问题:
- 浅比较的局限性:PureComponent和memo的默认浅比较无法检测嵌套对象的变更。解决方案是使用不可变数据更新模式,或者根据需要实现自定义比较函数。
- 过度优化问题:不必要的优化会使代码变得复杂,反而降低可维护性。建议先测量再优化,使用React DevTools分析组件渲染性能,重点关注实际瓶颈。
- 回调函数问题:在类组件中向优化后的子组件传递函数作为props时,如果使用内联函数,每次渲染都会创建新函数,导致优化失效。解决方案是使用类方法结合bind,或者使用useCallback缓存函数。
5.3 性能测量与调试
为了有效优化React应用性能,需要掌握性能测量工具:
- React DevTools Profiler:可以记录组件渲染时序和耗时,帮助识别不必要的渲染。
- 浏览器性能标签页:使用Chrome DevTools的Performance面板记录整体性能,分析JavaScript执行时间和布局重排。
- 自定义渲染计时:在开发环境中,可以使用以下代码手动测量组件渲染时间:
class MyComponent extends React.Component {
startTime = 0;
componentWillMount() {
this.startTime = performance.now();
}
componentDidMount() {
console.log(
`${this.constructor.name}渲染时间:`,
performance.now() - this.startTime, 'ms'
);
}
}
通过结合这些工具和方法,可以科学地识别性能瓶颈,评估优化效果,避免盲目优化。
6 总结
React的性能优化是一个复杂但重要的话题。通过合理使用shouldComponentUpdate、PureComponent和React.memo,可以显著减少不必要的渲染,提升应用性能。需要注意的是,这些优化手段都基于浅比较原理,因此必须配合不可变数据模式才能正常工作。
从源码层面理解这些优化机制的工作原理,有助于我们做出更合理的优化决策,避免常见陷阱。在实际项目中,应该遵循"先测量,后优化"的原则,专注于解决真正的性能瓶颈,而不是盲目地优化所有组件。
随着React的持续发展,函数组件和Hooks已经成为主流,React.memo结合useCallback和useMemo成为了最重要的优化手段。不过,无论API如何演变,性能优化的核心原则始终保持不变:只在必要时渲染,尽可能减少计算量。