React 中的 memo、useMemo 及 useCallback

距离我接触 react 已经过去几个月了，在此期间，关于如何避免重复渲染的问题一直困惑着我，因此今天就来聊聊这个话题。

在讲述如何进行性能优化之前，我们先来谈谈 React 为什么会重新渲染。

React 为什么会重新渲染

状态改变是 React 树内部发生更新的唯二原因之一。

Copy
import { useState } from "react";

const App = () => {
let [color, setColor] = useState("red");
return (

setColor(e.target.value)} />
<ExpensiveTree />

);
};

const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延迟
}
console.log('render');
return I am a very slow component tree. ;
};

很明显，每当我们在 input 里面输入内容，console.log('render')都会输出，因为 color 的状态发生了改变，也间接说明了其与 props 完全没有关系

这样的开销是很不合理的，我们接下来会优化它。

性能优化

方式一: State 抽离

我们知道 react 是单向数据流，因此我们只需要将 State 的抽离出来即可。

Copy
import { useState } from "react";

const App = () => {
return (

<Input />
<ExpensiveTree />

);
};

const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延迟
}
console.log("render");
return I am a very slow component tree. ;
};

const Input = () => {
let [color, setColor] = useState("red");

return setColor(e.target.value)} />;
}

方式二: memo

React.memo 其为高阶组件，可以使被它包裹的组件变为纯组件，也就是只要它的 prop 不改变，react 就不会更新它。

Copy
import { memo, useState } from "react";

const App = () => {
let [color, setColor] = useState("red");
return (

setColor(e.target.value)} />
<ExpensiveTree />

);
};

const ExpensiveTree = memo(() => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延迟
}
return I am a very slow component tree. ;
})

方式三: react children

因为 App 并没有发生状态改变所以 ExpensiveTree 避免了重复渲染

Copy
import { FC, PropsWithChildren, useState } from "react";

const App = () => {
return (
<ColorWrapper>
<ExpensiveTree />
</ColorWrapper>
);
};

const ColorWrapper: FC<PropsWithChildren> = ({ children }) => {
let [color, setColor] = useState("red");
return (

setColor(e.target.value)} />
{children}

);
};

const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延迟
}
return I am a very slow component tree. ;
};

使用 useMemo 和 useCallback

useMemo

useMemo 有点类似于 vue 中的 Computed，只有当依赖变化时，才会重新计算出新的值。

这样当 input 发生改变的时候 dirtyWork 就不会重复的去执行。

Copy
import { useMemo, useState } from "react";

const App = () => {
let [color, setColor] = useState("red");
const [number,setNumber] = useState(0)

const dirtyWork = useMemo(() => {
console.log('正在进行大量运输');
return number
},[number])

return (

setColor(e.target.value)} />
{dirtyWork}

);
};

另外上一节的例子我们也可以通过 useMemo 进行修改

Copy
import { memo, useMemo, useState } from "react";

const App = () => {
let [color, setColor] = useState("red");
return (

setColor(e.target.value)} />
{useMemo(
() => (
<ExpensiveTree />
),
[]
)}

);
};

const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延迟
}
return I am a very slow component tree. ;
};

useCallback

我们先看如下例子

Copy
import { FC, memo, useState } from "react";

const App = () => {
let [color, setColor] = useState("red");
const fn = ()=> {
console.log('hahaha');
}
return (

setColor(e.target.value)} />
<ExpensiveTree fn={fn}/>

);
};

const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延迟
}
console.log('render'); // 依旧会被不断更新
return I am a very slow component tree. ;
})

我们发现即使 ExpensiveTree 包裹 memo ，但在 input 里面输入内容， ExpensiveTree 依旧会被更新，这时我们只要给父组件 fn 函数包裹一层 useCallback 即可

因此 useCallback 一般用于需要将函数传递给子组件的情况，我们用 useCallback 改写上面的例子：

Copy
import { FC, memo, useCallback, useState } from "react";

const App = () => {
let [color, setColor] = useState("red");
const fn = useCallback(()=> {
console.log('hahaha');
},[])
return (

setColor(e.target.value)} />
<ExpensiveTree fn={fn}/>

);
};

const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延迟
}
console.log('render');
return I am a very slow component tree. ;
})

你可能会发现 useCallback 其实就是 useMemo 的语法糖，如上例子也可以使用 useMemo 改写

Copy
import { FC, memo, useMemo, useState } from "react";

const App = () => {
let [color, setColor] = useState("red");
const fn = useMemo(() => {
return () => console.log("hahaha");
}, []);
return (

setColor(e.target.value)} />
<ExpensiveTree fn={fn} />

);
};

const ExpensiveTree: FC<{ fn: () => void }> = memo(({ fn }) => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延迟
}
console.log("render");
return I am a very slow component tree. ;
});

参考资料

React 为什么重新渲染

Before You memo()