目前存在的问题
很多人在写函数组件(FC)时使用useMemo & useCallback不求甚解,好像在创建方法时只要使用useCallback wrap(声明变量使用useMemo wrap)就能达到性能优化的玄学,导致多数情况下useCallback & useMemo在代码里存在的原因仅仅是因为“大家都在用”。
如果不理解为什么我们要使用它,最好不要用它。
需要记住的一点是,useMemo & useCallback 只在re-render时期有效。在初始化渲染( initial render)时期, 不仅无用而且有害,它会使React做一些不必要的工作。这意味着在初始化渲染时期,使用useMemo & useCallback比不使用还要慢一点点🤏。但是如果整个工程有成千上万的地方使用,就能明显感知到这种性能影响。这两个hooks的作用就是为了实现渲染优化,如果大量错误地使用,反而与优化的初衷背道而驰。
前置知识:
首先我们需要知道导致React组件re-render的唯二两种情况:
-
props 或者states 改变(浅比较);
-
父组件重新渲染。
以及需要了解基本数据类型与引用数据类型:
'1' === '1' // true
[] === [] // false
{'1'} === {'1'} // false
还有一些hooks使用的基本规则:
-
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用;
-
只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
为什么需要useMemo & useCallback?
在每个re-render之间缓存值,避免在每次渲染时都进行高开销的计算,防止子组件进行不必要的渲染。
你是否是这样使用useMemo & useCallback的:
-
用useCallback包裹onClick防止重新渲染(一)
const Component = () => { const onClick = useCallback(() => { /* do something */ }, []); return ( <> <button onClick={onClick}>Click me</button> ... // some other components </> ); }; -
用useCallback包裹onClick防止重新渲染(二)
const Item = ({ item, onClick, value }) => <button onClick={onClick}>{item.name}</button>; const Component = ({ data }) => { const value = { a: someStateValue }; const onClick = useCallback(() => { /* do something on click */ }, []); return ( <> {data.map((d) => ( <Item item={d} onClick={onClick} value={value} /> ))} </> ); }; -
用useMemo包裹value防止重新创建onClick
const Item = ({ item, onClick }) => <button onClick={onClick}>{item.name}</button>; const Component = ({ data }) => { const value = useMemo(() => ({ a: someStateValue }), [someStateValue]); const onClick = useCallback(() => { console.log(value); }, [value]); return ( <> {data.map((d) => ( <Item item={d} onClick={onClick} /> ))} </> ); };
如果你一直是这样使用useMemo & useCallback,那么恭喜你!已经完美掌握了useMemo & useCallback无效的使用方法
如何正确有效使用useMemo & useCallback:
useMemo
pre-vuer可以将它理解为vue中的computed
依赖项为空则每次re-render都会触发
useMemo 和 useEffect 很相似,但是执行时机不同
useCallback
有且只有一种有效的情况:useCallback需要配合React.memo使用,即当组件的所有props和组件本身都被memoized。(其中如果有一个prop没有被memoized都是无效的)
-
以下是正确使用姿势的代码示例
const PageMemoized = React.memo(Page); const App = () => { const [state, setState] = useState(1); const onClick = useCallback(() => { console.log('Do something on click'); }, []); return ( // PageMemoized will NOT re-render because onClick is memoized <PageMemoized onClick={onClick} /> // page WILL re-render because value is not memoized // <PageMemoized onClick={onClick} value={[1, 2, 3]} /> // again 其中如果有一个prop没有被memoized都是无效的 ); }; useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
什么情况下使用?
useMemo:当需要缓存高开销计算的值
- 使用时需权衡:re-render不一定会发生,initial render一定会发生;
useCallback:防止子组件进行不必要的渲染,配合React.memo使用
后记
基于react组件更新的特性,部分开发者使用极端优化策略,秉承“宁可错杀一千,不肯放过一个”原则,memoize 所有的组件和 props,防止有“漏网之鱼”(即忘记被memoized的prop),导致整个memo失效。我不是很认同这个激进的优化方案,首先因为代码是人写的,总会有疏忽的时候,很难做到完美,而且也违背了克努特优化原则。
“过早优化是万恶之源” --- Tony Hoare
May this day be your last day in useMemo and useCallback hell!