React教程(十) : 性能优化 - useMemo, useCallback, memo
useMemo, useCallBack
这两个概念并非看上去那么容易理解,使用的不好的话,也很难带来任何的性能提升。
先说useMemo, 简单来说就是把返回值缓存起来,并监控一个变量。 如果被监控的变量不变,则返回值不变。以下是两个适用useMemo的场景
import React, { FC, useState, useMemo } from 'react';
export const UseMemoDemo:FC = (props) => {
const [age, setAge] = useState(0);
const [name, setName] = useState('Andy');
function timeCostingFun(){
console.log('call time costing fun');
return `blablalba - ${name}`;
}
return(
<>
<div> <NameComponent name={name}></NameComponent> <button onClick={()=>setName(`${name}${age}`)}>Update Name</button> </div>
<br />
<div>{`age - ${ age }`} <button onClick={()=>setAge(age+1)}>Add age</button> </div>
<br />
<div>{timeCostingFun()}</div>
</>
);
}
export interface NameProps{
name: string
}
const NameComponent:FC<NameProps> = (props) => {
function getDescription(){
console.log('call getName');
return `my name is ${props.name}`;
}
let desc = getDescription();
return(
<div>
{desc}
</div>
)
}
上例中,组件UseMemoDemo的timeCostingFun方法只与name有关,与age无关。但如果setAge,会触发render,也会再次调用timeCostingFun方法。
组件NameComponent的getDescription方法也会在父组件UseMemoDemo渲染的时候被重新渲染,而NameComponent也只与name有关,与age无关。
所以,这两处可以使用useMemo进行优化。优化修改后的代码如下所示:
import React, { FC, useState, useMemo } from 'react';
export const UseMemoDemo:FC = (props) => {
const [age, setAge] = useState(0);
const [name, setName] = useState('Andy');
// function timeCostingFun(){
// console.log('call time costing fun');
// return `blablalba - ${name}`;
// }
let result = useMemo(()=>{
console.log('call time costing fun');
return `blablalba - ${name}`;
}, [name]);
return(
<>
<div> <NameComponent name={name}></NameComponent> <button onClick={()=>setName(`${name}${age}`)}>Update Name</button> </div>
<br />
<div>{`age - ${ age }`} <button onClick={()=>setAge(age+1)}>Add age</button> </div>
<br />
{/* <div>{timeCostingFun()}</div> */}
<div>{result}</div>
</>
);
}
export interface NameProps{
name: string
}
const NameComponent:FC<NameProps> = (props) => {
// function getDescription(){
// console.log('call getName');
// return `my name is ${props.name}`;
// }
// let desc = getDescription();
let desc = useMemo(()=>{
console.log('call getName');
return `my name is ${props.name}`;
}, [props.name]);
return(
<div>
{desc}
</div>
)
}
上面这个示例非常的简单,因为useMemo所监控的变量类型都是原生类型(primitive),如果是Object类型呢?
于是我们对上述代码做如下修改,首先是子组件
为其props增加一个Object类型的字段,并让useMemo监控该字段。
其父组件做如下的修改
此时我们click Add age按钮,会发现即使user的值没有变化,子组件useMemo还是更新了。 这是因为父组件点击了Add age按钮后,重新渲染,并重新生成了一个userInitData,其值和之前的userInitData完全一样,但引用不一样,所以子组件的useMemo还是认为两者不等。
有个方案可以解决该问题
1: 将user对象展开为原生类型,如下图所示:
2: 将传入子组件的user值缓存起来(useState, useMemo都可以,具体看应用场景而定)
这样子组件的useMemo可以保持不变,继续监控props.user
参考:https://jancat.github.io/post/2019/translation-usememo-and-usecallback/
理解了useMemo之后,useCallback也就容易多了。 useMemo也是监控一组变量,只不过与useMemo不同,其返回的是方法。看下面的例子:
import React, { FC, useState, useCallback } from 'react';
export const UseCallbackDemo:FC = (props) => {
const [age, setAge] = useState(0);
const [name, setName] = useState('Andy');
return(
<>
<div> <NameComponent name={name} click={()=>{
console.log('click name button');
}}></NameComponent></div>
<br />
<div>{`age - ${ age }`} <button onClick={()=>setAge(age+1)}>Add age</button> </div>
</>
);
}
interface NameProps{
name: string,
click: ()=>void
}
const NameComponent:FC<NameProps> = (props) => {
console.log('call name component')
return(
<button onClick={props.click}>
{props.name}
</button>
)
}
子组件NameComponent接受click方法作为props的一部分。该demo中,点击夫组件的Add age按钮,还是会导致子组件刷新。 如果做到子组件的props不变,子组件就不重新渲染呢?
答案是:使用React.memo和useCallback。 代码示例如下:
React.memo的作用就是缓存一个function,如果其props不变,就返回缓存值。
完整源码地址:https://github.com/992990831/modernization/tree/main/performance-tuning