memo
类组件可以通过继承类 PureComponent
或者实现 shouldComponentUpdate
来主动判断组件是否需要重新渲染,以此来提高性能。在使用函数组件的时候可以使用memo
达到此目的。
React.memo
接收两个参数,一个是组件,一个是(比较)函数- 组件:组件必须是函数式组件
- 函数:这个函数就是定义组件是否需要重渲染的钩子,该函数传入两个参数,第一个参数为上次渲染的
props
,第二参数为本次渲染的props
- 函数返回值为
true
时复用最近一次渲染,否则false
重新渲染
⚠️ 注意:
如果不通过比较函数进行比较,那么依然是一种对象的浅比较,有复杂对象时无法重新渲染
举个例子
import { useState } from 'react';
const Com1 = (props: any) => {
console.log(`Com1 ---- ${props.count}`)
return <div> {props.count}</div>;
};
const Com2 = (props: any) => {
console.log(`Com2 ---- ${props.count}`)
return <div> {props.count}</div>
};
const App = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<>
<Com1 count={count1} />
<button onClick={() => setCount1(count1 + 1)}>count1 + 1</button>
<Com2 count={count2} />
<button onClick={() => setCount2(count2 + 1)}>count2 + 1</button>
</>
);
};
export default () => <App />;
当你点击count2 + 1
按钮,控制台打印如下:
Com1
也触发了render
!
明明点击的是count2 + 1
按钮,我只希望修改count2
,也只希望Com2
组件重新render
。
不必要的render
产生了! ❌
当你使用了memo
后:
import { memo, useState } from 'react';
const Com1 = (props: any) => {
console.log(`Com1 ---- ${props.count}`)
return <div> {props.count}</div>;
};
const Com2 = (props: any) => {
console.log(`Com2 ---- ${props.count}`)
return <div> {props.count}</div>
};
const MemoCom1 = memo(Com1);
const MemoCom2 = memo(Com2);
const App = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<>
<MemoCom1 count={count1} />
<button onClick={() => setCount1(count1 + 1)}>count1 + 1</button>
<MemoCom2 count={count2} />
<button onClick={() => setCount2(count2 + 1)}>count2 + 1</button>
</>
);
};
export default () => <App />;
点击count2 + 1
按钮:
只有Com2
触发了render
!
避免了Com1
组件不必要的渲染 ✅
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- 把 创建 函数和依赖项数组作为参数传入
useMemo
,它仅会在某个依赖项改变时才重新计算memoized
值。这种优化有助于避免在每次渲染时都进行高开销的计算。 - 记住,传入
useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于useEffect
的适用范畴,而不是useMemo
。
⚠️ 注意:
- 如果没有提供依赖项数组,
useMemo
在每次渲染时都会计算新的值。
你可以把 useMemo
作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择 遗忘 以前的一些 memoized
值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码,之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
举个例子
import { memo, useState } from 'react';
const Com1 = (props: any) => {
console.log(`Com1 ---- ${props.count}`)
return <div> {props.count}</div>;
};
const Com2 = (props: any) => {
console.log(`Com2 ---- ${props.count}`)
return <div> {props.count}</div>
};
const Com3 = (props: any) => {
console.log(`Com3 ---- ${props.count}`)
return <div>{props.count}</div>;
};
const MemoCom1 = memo(Com1);
const MemoCom2 = memo(Com2);
const MemoCom3 = memo(Com3);
const App = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const count3 = count1 + Math.random();
return (
<>
<MemoCom1 count={count1} />
<button onClick={() => setCount1(count1 + 1)}>count1 + 1</button>
<MemoCom2 count={count2} />
<button onClick={() => setCount2(count2 + 1)}>count2 + 1</button>
<MemoCom3 count={count3} />
</>
);
};
export default () => <App />;
上面的例子中count3
只依赖了count1
并作为prop
传入了MemoCom3
组件。
点击count2 + 1
按钮:
Com3
也触发了render
!
我们希望只在count1
变化的时候重新计算count3
并重新渲染MemoCom3
组件。
不必要的render
产生了! ❌
当使用useMemo
后:
import { memo, useState, useMemo } from 'react';
const Com1 = (props: any) => {
console.log(`Com1 ---- ${props.count}`)
return <div> {props.count}</div>;
};
const Com2 = (props: any) => {
console.log(`Com2 ---- ${props.count}`)
return <div> {props.count}</div>
};
const Com3 = (props: any) => {
console.log(`Com3 ---- ${props.count}`)
return <div>{props.count}</div>;
};
const MemoCom1 = memo(Com1);
const MemoCom2 = memo(Com2);
const MemoCom3 = memo(Com3);
const App = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const memoCount3 = useMemo(() => {
return count1 * Math.random();
}, [count1]);
return (
<>
<MemoCom1 count={count1} />
<button onClick={() => setCount1(count1 + 1)}>count1 + 1</button>
<MemoCom2 count={count2} />
<button onClick={() => setCount2(count2 + 1)}>count2 + 1</button>
<MemoCom3 count={memoCount3} />
</>
);
};
export default () => <App />;
点击count2 + 1
按钮:
只有Com2
触发了render
!
避免了Com3
组件不必要的渲染 ✅
useCallback
const memoizedCallback = useCallback(() => {
setValue(value);
}, [value]);
- 返回一个
memoized
回调函数 - 把 内联回调函数 及 依赖项数组 作为参数传入
useCallback
,它将返回该回调函数的memoized
版本,该 回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用 引用相等性 去避免非必要渲染(例如shouldComponentUpdate)
的子组件时,它将非常有用。 useCallback(fn, deps)
相当于useMemo(() => fn, deps)
。
⚠️ 注意:
- 依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
- 推荐启用
eslint-plugin-react-hooks
中的exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
举个例子
import { memo, useState, useCallback, useMemo } from 'react';
const Com1 = (props: any) => {
console.log(`Com1 ---- ${props.count}`)
return <div> {props.count}</div>;
};
const Com2 = (props: any) => {
console.log(`Com2 ---- ${props.count}`)
return <div> {props.count}</div>
};
const Com3 = (props: any) => {
console.log(`Com3 ---- ${props.count}`)
return <div>{props.count}</div>;
};
const Com4 = (props: any) => {
console.log(`Com4 ---- `)
return <button onClick={props.handleCount3}>count1 * 2 + 1</button>;
};
const MemoCom1 = memo(Com1);
const MemoCom2 = memo(Com2);
const MemoCom3 = memo(Com3);
const MemoCom4 = memo(Com4);
const App = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const memoCount3 = useMemo(() => {
return count1 * Math.random();
}, [count1]);
const handleCount3 = (e: any) => {
console.log('handleCount3', e.target);
setCount1(count1 + 1);
}
return (
<>
<MemoCom1 count={count1} />
<button onClick={() => setCount1(count1 + 1)}>count1 + 1</button>
<MemoCom2 count={count2} />
<button onClick={() => setCount2(count2 + 1)}>count2 + 1</button>
<MemoCom3 count={memoCount3} />
<MemoCom4 handleCount3={handleCount3} />
</>
);
};
export default () => <App />;
上面的例子中handleCount3
函数的函数体依赖了count1
,作用是修改count1
并作为prop
传入MemoCom4
组件。
点击count2 + 1
按钮:
Com4
也触发了render
!
我们只希望在count1
需要改变之前拿到上一个count1
,并重新声明handleCount3
函数传入MemoCom4
组件触发重新渲染。
不必要的render
产生了! ❌
使用useCallback
后:
import { memo, useState, useCallback, useMemo } from 'react';
const Com1 = (props: any) => {
console.log(`Com1 ---- ${props.count}`)
return <div> {props.count}</div>;
};
const Com2 = (props: any) => {
console.log(`Com2 ---- ${props.count}`)
return <div> {props.count}</div>
};
const Com3 = (props: any) => {
console.log(`Com3 ---- ${props.count}`)
return <div>{props.count}</div>;
};
const Com4 = (props: any) => {
console.log(`Com4 ---- `)
return <button onClick={props.handleCount3}>count1 * 2 + 1</button>;
};
const MemoCom1 = memo(Com1);
const MemoCom2 = memo(Com2);
const MemoCom3 = memo(Com3);
const MemoCom4 = memo(Com4);
const App = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const memoCount3 = useMemo(() => {
return count1 * 2;
}, [count1]);
const handleCount3 = (e: any) => {
console.log('handleCount3', e.target);
setCount1(count1 + 1);
};
const callbackHandleCount3 = useCallback((e) => {
handleCount3(e);
}, [count1]);
return (
<>
<MemoCom1 count={count1} />
<button onClick={() => setCount1(count1 + 1)}>count1 + 1</button>
<MemoCom2 count={count2} />
<button onClick={() => setCount2(count2 + 1)}>count2 + 1</button>
<MemoCom3 count={memoCount3} />
<MemoCom4 handleCount3={callbackHandleCount3} />
</>
);
};
export default () => <App />;
点击count2 + 1
按钮:
只有Com2
触发了render
!
避免了Com4
组件不必要的渲染 ✅
总结
memo
、useMemo
、useCallback
都是react
项目性能优化的手段。但是当你编写庞大的组件之前也可以考虑是否有其他方式去减少不必要的渲染,比如状态下移和内提升,感兴趣的可以去看Dan
的这篇文章:在你写memo()之前。