Hooks是React 16.8版本的新增特性,它的出现让我们可以不再写class组件来维护组件的内部状态。 在Hooks我们常用的基础方法为useState和useEffect,而对于useCallback和useMemo这两个方法,大家看到它的第一眼想到的可能就是性能优化吧。那么这两个方法是不是适用于所有的场景呢,这就是我们今天想要探讨的问题。
useMemo
1 const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo缓存计算结果,它接收一个计算的过程(回调函数,它将返回结果)和依赖项数据,返回一个memoized值。当依赖项发生变化的时候,回调函数会重新计算。
useCallback
1 2 3 4 5 6 const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
useCallback缓存一个函数体,它接收回调参数和依赖项数组,返回一个 memoized 回调函数,只有依赖项发生变化的时候才会返回一个新的函数。
那么是不是所有场景下使用useCallback都能达到性能优化的效果呢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function Example ( ) { const [value, setValue] = useState(); const onChange = (e ) => { setValue(e.target.value); }; return <input value ={value} onChange ={onChange} /> ; } ---------------------------------------------------------------------------------------------- const onChange = useCallback(e => { setValue(e.target.value); }, []); const onChange = (e ) => { setValue(e.target.value); }; const onChangeMemoized = useCallback(onChange, []);
我们给一个input框传入onChange方法,当我们将它加上useCallback后,我们会发现这个方式除了定义了onChange方法外,还有调用useCallback产生了额外的开销,导致适得其反。 可能会有同学有疑问,我们不是用了useCallback吗,为啥onChange还会重新定义呢。这是因为函数组件每次state一变化,就重新执行,会重复声明。useCallback会缓存之前传入的回调函数,但是一旦依赖项发生变化,将返回新的函数。 实际上,useCallback在很多时候需要和React.memo搭配使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const BigData = ({ showNum } ) => { const [num, setNum] = useState(() => showNum()); console .log("子组件重新渲染了喔" ); useEffect(() => { setNum(showNum()); }, [showNum]); return ( <div className='BigData' > {'child:' +num} <br></br> {'假设子组件渲染大量数据...'} </ div> ); }; export default React.memo(BigData)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import BigData from './Child' const App = () => { const [value, setValue] = useState('' ); const [num, setNum] = useState(1 ); const showNum = useCallback(() => { return num; }, [num]); return ( <div className='app' > <div>{'parent:' + num}</div> <div> <button onClick={() => setNum(num * 2)}>*2</ button> </div> <input value={value} onChange={event => setValue(event.target.value)} / > <BigData showNum={showNum} /> </div> ); }
我们假设一个场景,子组件需要展示大量的数据,它从父组件接收一个函数。在很多时候,父组件更新的时候,我们不需要子组件的更新。可能大家会给子组件包装到React.memo中(作用可参考shouldComponentUpdate(),但仅适用于函数组件),来保证props相同的情况下不重复渲染组件。但是函数式组件更新的时候函数会重新声明,引用发生了变化。而React.memo函数只会浅比较props,因此子组件仍然会重新渲染。此时我们给要传入子组件的函数加上useCallback来保证函数引用的相等,从而达到子组件不重复渲染的效果,实现性能优化。 我们来看一下不加useCallback的时候 可以看到state改变父组件重新渲染的时候,子组件也重新渲染了。
如果加上useCallback 可以看到state改变父组件重新渲染的时候,子组件没有重新渲染,达到了我们想要的效果。
那么useMemo其实也是类似的,当我们需要给子组件传入一个引用类型的对象时,父组件重新渲染会导致值的引用发生变化。如果此时我们不需要重新渲染子组件时,可以用useMemo来记住这个值。
1 2 3 4 5 6 7 const Example = () => { const value = useMemo(() => { compute(num) }, [num]); return <BigData value ={value} /> }
我们假设渲染子组件的开销较大(又是一个渲染大量数据的组件2333),那么value(返回值为引用类型)的引用变化而依赖项num没有变化时,我们可能不想子组件重新渲染。因此可以用useMemo来避免Example组件的渲染导致compute方法重新计算。此时value的引用不会发生变化,子组件不会重新渲染。 我们来看一个具体的例子
1 2 3 4 5 6 7 8 9 10 11 const ChildMemo = ({ childData, onClick} ) => { console .log('我是子组件,我渲染了he' ) return ( <div> <span >{'子组件:' +childData.name}</span> <button onClick={() => onClick('变身2333')}>改变name</ button> </div> ); } export default React.memo(ChildMemo);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const Example = () => { const [num, setNum] = useState(0 ); const [name, setName] = useState('子组件' ); return ( <> <span>number:{num}</span> <button onClick={(e) => { setNum(num + 1) }}>加1</ button> <ChildMemo childData={ useMemo(() => ({ name, color: name.indexOf('2333' ) !== -1 ? 'blue' : 'purple' }), [name]) } onClick={useCallback((newName ) => setName(newName), [])} /> </> ) }
当我们点击按钮时,如果childData返回值不加上useMemo,由于传入的参数为引用类型,引用变化会导致子组件的重新渲染。这种场景和上一个例子相似,只不过传入的参数不是一个方法,而是一个引用类型的值了。用useMemo可以保证在依赖项不变的时候,传入子组件的是同一个引用。
关于useMemo,还有一种情况我们可以使用。当一个函数的开销很大时(有较复杂的计算过程),我们可以用useMemo来记住它的返回值,这样可以避免性能消耗较高的重复计算。
1 2 3 4 5 6 7 8 9 10 11 const Example = () => { const result = useMemo(() => expensiveCompute(value), [value]); function expensiveCompute (value ) { } return ( <div> {result} </div> ) }
加上useMemo后,虽然组件在重新渲染的时候将会重新定义这个开销较大的函数,但是它只会在被需要的时候才会被调用。当依赖项不变时,该方法将返回之前已经计算好的值。
总结 useCallback和useMemo的使用场景可以大致有以下两种。 1、保证传入子组件的引用相等 当子组件或者需要接收父组件传来的函数、对象、数组等引用类型时,或者它们被用在其他hook中的依赖数组中,我们应该使用。 2、开销大的运算 使用useMemo避免重复计算相同的结果。