React.memo的使用背景
关于这个使用和使用背景,网上是有清晰的答案的。前提是通常情况下props发生改变时,react就会re-render。也就是说当你的子组件不想被父组件影响重新渲染时,你就需要给子组件一个react.memo包裹起来,使其当props发生改变时 才会re-render。但是什么时候才使用它呢,是不是所有子组件都要使用它,网上没有很清晰的答案。笔者以下将会做出自己的总结。
react.memo
能减少重绘次数,把组件传入的props做一个lodash缓存,每次会做一个浅比较,当props改变时,会与缓存的内容做一个浅比较(当然浅比较也是要消费性能的,memo还是要慎用)也就是说当传入的值时useState定义的内容(useState 是不会在每次重新渲染时都重新初始化的,只有在组件重载时才会初始化,所以引用地址一样),数值,字符串,布尔值时,能避免不必要的re-render。但是因为做了浅比较,这又产生了一个问题。react.memo不生效的情况。
注:react 内部流程,其实跟vue差不多只不过没有双向数据流绑定的这个过程:Data->component->VDOM->DOM
memo比较的其实时虚拟dom,这个过程相对于re-render非常快,所以不用担心效率问题,能很好的阻止页面无效重绘重排
react.memo不生效的情况
复杂数据类型当比较到存储地址不一致时,因为父组件的重新render导致了状态行为重新定义,就会判断不一致从而导致子组件re-render,这里我试验了一些例子:
const [content, setContent] = useState(0);
const [other, setOther] = useState(0);
const [contentObj, setContentObj] = useState([{a: 1}, {a: 2}]);
const handleClick = () => {
console.log('父组件点击');
setContent((content) => {
return content + 1;
})
setContentObj([{a: 2}, {a: 4}])
}
const otherHandle = () => {
setOther(other=>(other+1))
}
const obj = useMemo(() => {
return {a: content}
}, [content])
const func = function () {
console.log('点击点击')
}
<Button onClick={handleClick}>父组件点击{content}</Button>
<Button onClick={otherHandle}>额外点击{other}</Button>
{/* 传数值,子组件不触发 */}
{/* <NewTestComponent params={content}/> */}
{/* 子组件不触发 */}
<NewTestComponent params={contentObj}/>
{/* 传对象 params={{a: content}},子组件触发了,用react.memo包裹起来就不触发了 */}
{/* <NewTestComponent params={obj}/> */}
{/* 传函数,子组件触发 */}
{/* <NewTestComponent params={func}/> */}
{/* 传内联函数,子组件触发 */}
{/* <NewTestComponent params={() => {}}/> */}
不生效的情况下,就用到了useMemo和useCallback。还有一种情况,如果是静态的复杂类型参数,也就是说不需要在props中更改的静态复杂类型参数,还要用useMemo嘛,不,这里不是要记住一个值是要在重新渲染时保持对值的引用不变,所以这里用useRef()更好一点。
const obj = useRef(['参数1', '参数2']);
useMemo
当处理react.memo不生效的情况时,不需要使用useMemo, 也就是当传入的数值是object, array时要用useMemo。
其他情况,是为了缓存在渲染过程中比较繁重的计算过程,当处理复杂的计算逻辑时要用到useMemo。
useCallback
当处理react.memo不生效的情况时,当传入的值是函数时,都要用useCallback。
当组件刷新时,未被usecallback包裹的方法将被垃圾回收并重新定义,但被usecallback所制造的闭包将保持对回调函数喝依赖项的引用。
usecallback,设计的初衷并非解决组件内部函数多次创建的问题,而是减少子组件的不必要重复渲染。
react优化思路:
1.减少重新 render 的次数。因为 React 最耗费性能的就是调和过程(reconciliation),只要不 render 就不会触发 reconciliation。
2.减少计算量
总结:
useCallBack不要每个函数都包一下,否则就会变成反向优化,useCallBack本身就是需要一定性能的
useCallBack并不能阻止函数重新创建,它只能通过依赖决定返回新的函数还是旧的函数,从而在依赖不变的情况下保证函数地址不变
useCallBack需要配合React.memo使用
是否每个子组件都需要使用react.memo
答案是参差不齐的,应该说没有一个标准答案,但是就我本人而言,当props极其复杂且render函数又很简单的时候直接re-render就好了,没必要重新使用memo缓存,因为重新计算props再进行浅比较可能代价比re-render还要高。当组件执行 render 函数代价很高(比如每次 render 需要跑一些有的没的运算),那就可以考虑用 React.memo 缓存组件的状态。每个人有每个人不同的优化方式和开发习惯,这也是为什么memo没有被react内置的原因吧。
上面既然提到了浅比较,那么浅比较又是什么呢?
浅比较中两个对象相同指的是:两个对象的属性个数相同且两个对象的属性相同,并且两个对象的属性值也一样。属性值一样,指的是引用一样,不再深度比较这两个对象里面的东西是否一样了。
比如:
var obj = {a:1};
obj1 = obj;
obj1 === obj; // true
这是引用地址一样
{a: 1} === {a: 1}; //false
这是引用地址不一样
浅比较源码:
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
- Performs equality by iterating through keys on an object and returning false
- when any key has values which are not strictly equal between the arguments.
- Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 调用Object.is判断是否相等,相同返回true,不同返回false
if (Object.is(objA, objB)) { // 判断简单数据类型,+0,-0, NaN
return true;
}
// object.is比较发现不等,但并不代表真的不等,object对象还需要比较
// 这里判断是否是object,如果不是,那直接返回false
if (
typeof objA !== ‘object’ ||
objA === null ||
typeof objB !== ‘object’ ||
objB === null
) {
return false;
}
// 判断负责数据类型
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// 比较对象中的keys长度,不等返回false
if (keysA.length !== keysB.length) {
return false;
}
// 比较对象中相同的key的val是否相等
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) || // 判断在objB中是否有objA中的所有key,比较key是否相同
!Object.is(objA[keysA[i]], objB[keysA[i]]) // 判断同key的value是否相同
) {
return false;
}
}
return true;
}
// 浅比较函数
// 比较了props和nextProps,state和nextState
function shallowCompare(instance, nextProps, nextState) {
return (
!shallowEqual(instance.props, nextProps) ||
!shallowEqual(instance.state, nextState)
);
}
// Object.is
// 如果x === y相等时,返回x !== 0 || 1 / x === 1 / y
// 否则返回x !== x && y !== y
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
);
}
/*
- 为什么要这么比较?可以参考下面的图
*/
// x === y 的时候,比较了类型和值
// 但,+0 === -0 ,结果为true,但我们希望结果应该是false
// NaN === NaN,结果为false,但我们希望结果应该是true
// 当 +0 === -0 进入判断体中,再比较x!==0,结果为false,+1/0 === -1/0 => Infinity === -Infinity,结果就为false
// 当不相等的时候,进入第二个判断体,此时NaN比较则返回了true,然后x与y做&&比较,返回结果