React-渲染控制
在React中进行渲染控制来进行优化,实际优化的并不是浏览器渲染 UI 的过程,而是优化前置的js执行过程。在数据量大、数据改变频繁的场景,对不必要的渲染进行控制是非常有必要的。
我们可以从以下几个层面规避不必要的渲染:
对 React.element 对象进行缓存
先看看下面的例子:
export default function Father() {
const [childProps, setChildProps] = React.useState({});
const [other, setOther] = React.useState(0);
return (
<div>
<Children {...childProps} />
<button onClick={() => setChildProps({ value: 1 })}>
Change Children's Props
</button>
<button onClick={() => setOther(numberB + 1)}>Change other</button>
</div>
);
}可以看到,Children组件的数据显示依赖于childProps的数据变化。不过当我们点击第二个按钮时,虽然改变的不是Children所依赖的值,但是Children会重新渲染。如果渲染Children的开销比较大,就会造成一定程度的性能问题。
解决这个问题的方式也很简单,我们可以将Children对应的React.Element对象使用useMemo缓存起来:
```jsx
export default function Father(){
const [ childProps , setChildProps ] = React.useState({});
const [ other, setOther ] = React.useState(0);
const _Children = useMemo(() => (
<Children {...childProps} />
),[childProps])
return <div>
<_Children />
<button onClick={ ()=> setChildProps({ value: 1}) } >
Change Children's Props
</button>
<button onClick={ ()=> setOther(numberB + 1) } >
Change other
</button>
</div>
}PureComponent 的使用
在类组件中,可以使用React.PureCompent来规避一些不必要的渲染。
其规避重复渲染的原理大致是:在触发渲染的时候会对其自身的state和props和上一状态下的state和props进行浅比较。所谓浅比较,即基本数据类型比较值,引用数据类型比较引用。如果浅比较有差异就重新渲染,不然就不重新渲染。其内部核心源码实现如下:
function checkShouldComponentUpdate() {
if (typeof instance.shouldComponentUpdate === "function") {
return instance.shouldComponentUpdate(
newProps,
newState,
nextContext
); /* shouldComponentUpdate 逻辑 */
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
}从上面的代码也可以看出,shouldComponentUpdate钩子会在浅比较之前执行。
下面是React的浅比较代码实现:
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
//直接比较新老 props 或者新老 state 是否相等。如果相等那么不更新组件。
return true;
}
if (
typeof objA !== "object" ||
objA === null ||
typeof objB !== "object" ||
objB === null
) {
//判断新老 state 或者 props ,有不是对象或者为 null 的,那么直接返回 false ,更新组件
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
//判断数组的长度是否相等,如果不相等,证明有属性增加或者减少
return false;
}
// Test for A's keys different from B.
// 遍历老 props 或者老 state ,判断对应的新 props 或新 state ,有没有与之对应并且相等的。如果有一个不对应或者不相等,那么直接返回 false
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}当父组件是函数组件时,传入props的引用类型都应该使用useMemo和useCallback进行缓存引用,不然会造成PureComponent失效的情况。
类组件的 shouldComponentUpdate 钩子
在类组件中,我们可以通过shouldComponentUpdate来控制组件是否渲染。它传入两个参数nextProps和nextState。我们可以利用这些信息与当前的state和props进行比较,决策是否要进行渲染。如果返回值为true则会重新渲染,返回值为false则不会。
在官方文档中,更加推崇使用PureComponent:
在大部分情况下,你可以继承
React.PureComponent以代替手写shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了shouldComponentUpdate()的实现。
一般来说,PureComponent所进行的浅比较也已经够用,若有深比较来控制是否渲染的需求,可以在shouldComponentUpdate自行实现。不过,官方文档中也是不建议这么去做:
我们不建议在
shouldComponentUpdate()中进行深层比较或使用JSON.stringify()。这样非常影响效率,且会损害性能。
React.memo 的使用
React.memo是一个高阶组件,它的用法如下:
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});其内部实现源码如下(移除开发版本代码):
import { REACT_MEMO_TYPE } from "shared/ReactSymbols";
// case REACT_MEMO_TYPE:
// fiberTag = MemoComponent;
import isValidElementType from "shared/isValidElementType";
export function memo<Props>(
type: React$ElementType,
compare?: (oldProps: Props, newProps: Props) => boolean
) {
const elementType = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
return elementType;
}可以看到,这个方法其实是由两个参数的,第二个参数compare传入一个函数,这个函数的两个参数分别是更新前和更新后的props。可以在这里写入控制渲染的逻辑,最终这段逻辑会写入React.element对象中(如果不传第二个参数,对应的elementType的compare为null)。
被React.memo包裹的组件会被打上MemoComponent的fiberTag,即转换为fiber后会被标记为MemoComponent。下面是React对带有该标记的fiber的处理逻辑(移除开发版本代码):
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes
): null | Fiber {
if (current === null) {
const type = Component.type;
if (
isSimpleFunctionComponent(type) &&
Component.compare === null &&
// SimpleMemoComponent codepath doesn't resolve outer props either.
Component.defaultProps === undefined
) {
let resolvedType = type;
// If this is a plain function component without default props,
// and with only the default shallow comparison, we upgrade it
// to a SimpleMemoComponent to allow fast path updates.
workInProgress.tag = SimpleMemoComponent;
workInProgress.type = resolvedType;
return updateSimpleMemoComponent(
current,
workInProgress,
resolvedType,
nextProps,
renderLanes
);
}
const child = createFiberFromTypeAndProps(
Component.type,
null,
nextProps,
workInProgress,
workInProgress.mode,
renderLanes
);
child.ref = workInProgress.ref;
child.return = workInProgress;
workInProgress.child = child;
return child;
}
const currentChild = ((current.child: any): Fiber); // This is always exactly one child
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes
);
if (!hasScheduledUpdateOrContext) {
// This will be the props with resolved defaultProps,
// unlike current.memoizedProps which will be the unresolved ones.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
const newChild = createWorkInProgress(currentChild, nextProps);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}其中对compare的处理如下:
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}如果传入的compare为null(即React.memo不传第二个参数的情况),会对更新前和更新后的props进行浅比较。所以一般来说,我们可以把React.memo当成官方提供的针对函数组件的PureComponent方案。
