大家好,我是张添财。上篇更新了memo的用法,但是在文章结尾处我列举的demo中虽然子组件用memo包裹了,但是父组件渲染仍造成了子组件的rerender。具体原因也给大家分析了,有兴趣的看下我的这篇更文《React性能优化手段Memo防止子组件不必要reRender》。今天就给小伙伴们填填坑,掰扯掰扯useMemo、useCallback是怎么用的。(一定要注意一点,useMemo、useCallback做性能优化时子组件要用memo包裹,没有这个前提,useMemo、useCallback优化是没用的!)
一、useMemo:解决父组件传给子组件的参数是复杂数据类型,子组件仍会渲染的问题
1、使用场景:
假设以下场景,父组件在调用子组件时传递 info 对象属性,点击父组件的点击增加按钮时,发现控制台会打印出子组件被渲染的信息。
import React, { memo, useState } from 'react';
// 子组件
const ChildComp = (props:{info:{name, age}}) => {
console.log('ChildComp...',name,age);
return (<div>ChildComp...</div>);
};
const MemoChildComp = memo(ChildComp);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = { name, age };
return (
<div className="App">
<div>hello world {count}</div>
<button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
<MemoChildComp info={info}/>
</div>
);
};
export default Parent;
分析原因:
点击父组件按钮,触发父组件重新渲染;父组件渲染,const info = { name, age } 一行会重新生成一个新对象,导致传递给子组件的 props 变化,进而导致子组件重新渲染。
解决方法:
使用 useMemo 将对象属性包一层。useMemo 有两个参数:
- 第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象;
- 第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。 下面请看改进后的代码:
import React, { memo, useMemo, useState } from 'react';
// 子组件
const ChildComp = (info:{info:{name: string, age: number}}) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};
const MemoChildComp = memo(ChildComp);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
// 使用 useMemo 将对象属性包一层
const info = useMemo(() => ({ name, age }), [name, age]);
return (
<div className="App">
<div>hello world {count}</div>
<button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
<MemoChildComp info={info}/>
</div>
);
};
export default Parent;
2、解释useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo返回一个被记忆的值 !!!。
注意传入 useMemo的依赖项,这样的话useMemo它仅会在某个依赖项改变时才重新计算 memoized 值
,这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
二、useCallback:包裹函数,依赖改变时返回新的函数
1、用法:
useCallback的第一个参数是函数体,第二个参数是依赖项,只有依赖项中的变量改变时,才会返回一个新的函数
const myFunction=( ()=>{
函数体...
}, [])
demo案例:
紧接着上面useMemo的例子,假设需要传给子组件一个函数,如下所示,当点击父组件按钮时,发现控制台会打印出子组件被渲染的信息,说明子组件又被重新渲染了。
import React, { memo, useMemo, useState } from 'react';
// 子组件
const ChildComp = (props) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};
const MemoChildComp = memo(ChildComp);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = useMemo(() => ({ name, age }), [name, age]);
const changeName = () => {
console.log('输出名称...');
};
return (
<div className="App">
<div>hello world {count}</div>
<button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
<MemoChildComp info={info} changeName={changeName}/>
</div>
);
};
export default Parent;
分析原因:
点击父组件按钮,改变了父组件中 count 变量值(父组件的 state 值
),进而导致父组件重新渲染;父组件重新渲染时,会重新创建 changeName 函数,即传给子组件的 changeName 属性发生了变化,子组件props发生变化从而导致子组件渲染;
解决方法:
修改父组件的 changeName 方法,用 useCallback 钩子函数包裹一层, useCallback 参数与 useMemo 类似
使用 useCallback 将函数包一层,useCallback 有两个参数:
- 第一个参数是个函数,返回的函数指向同一个引用,不会创建新函数;
- 第二个参数是个数组,第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,就会重新返回一个新的记忆函数提供给后面进行渲染。如果是一个空数组则是无论什么情况下该函数都不会发生改变。
import React, { memo, useCallback, useMemo, useState } from 'react';
// 子组件
const ChildComp = (props) => {
console.log('ChildComp...');
return (<div>ChildComp...</div>);
};
const MemoChildComp = memo(ChildComp);
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
const [name] = useState('jack');
const [age] = useState(11);
const info = useMemo(() => ({ name, age }), [name, age]);
const changeName = useCallback(() => {
console.log('输出名称...');
}, []);
return (
<div className="App">
<div>hello world {count}</div>
<button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
<MemoChildComp info={info} changeName={changeName}/>
</div>
);
};
export default Parent;
优化后分析:
用useCallback包裹后,父组件render时,包裹后的函数因为依赖项不变所以还是用记忆函数,则MemoChildComp组件中changeName并没有发生改变,那个子组件props也没有改变,也就不会进行rerender。
补充: useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。
useCallback(fn, inputs) 就相当于 useMemo(() => fn, inputs).
前面使用 useCallback 的例子可以使用 useMemo 进行改写:
这是通过useMemo进行改写上面useCallback这个例子。
const changeName = useMemo(() => () => {
console.log('输出名称...');
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
唯一的区别是:useCallback不会执行第一个参数函数
,而是将它返回给你,而useMemo会执行第一个函数并且将函数执行结果返回给你
。
总结: useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值。
写在最后:
最后再给大家总结下各钩子的使用场景。
- useState:涉及到
React状态的存取改变时
进行使用;state改变时会触发render,需要注意的是只能用setState来改变状态值。 - useEffect:
处理副作用
的钩子,场景上可以模拟类写法中的生命周期来进行使用。 - useRef:
获取DOM元素或保存变量
,可以绕过capture Value特性,也就是说无论在哪里取值,useRef保存的变量都是最新的。 - useImperativeHandle: 父组件调用子组件的属性,要结合forwardRef使用。
- memo、useMemo、useCallback:三者结合使用,可以有效的减少项目里组件的rerender,是React的一种性能优化手段。
到这里我们hooks系列就暂时告一段落了,这篇文章也算是给整个hooks系列收了收尾。其实在实际业务场景中,用这几个钩子就已经绰绰有余了。后续的话可能会写一些偏业务相关的。也欢迎各位小伙伴评论区留言,有其他感兴趣的话题我们也可以共同讨论下。好了,本系列到此结束,各位同好继续加油呐💪