useMemo

useMemo

useMemo 是拿来保持一个对象引用不变的。useMemo 和 useCallback 都是 React 提供来做性能优化的。比起 classes, Hooks 给了开发者更高的灵活度和自由,但是对开发者要求也更高了,因为 Hooks 使用不恰当很容易导致性能问题。

假设有个 component,在 dataConfig 变化的时候重新去 fetchData:

1
2
3
4
5
6
<Child
fetchData={() => {
// fetch data
}}
dataConfig={{ id: getId(queryId) }}
/>

如果是个 Class Component,会这么写:

1
2
3
4
5
6
7
class Child extends React.Component<Props> {
componentWillReceiveProps(nextProps: Props) {
if (nextProps.dataConfig !== this.props.dataConfig) {
nextProps.fetchData(nextProps.dataConfig)
}
}
}

使用 Hooks 后长这样:

1
2
3
4
5
const Child = ({ fetchData, dataConfig }: Props) => {
useEffect(() => {
fetchData(dataConfig)
}, [fetchData, dataConfig])
}

使用 Class Component 时我们需要手动管理依赖,但是使用 Hooks 时会带来副作用:React 使用的是Object.is(),如果fetchData的 reference 变了,也会触发 useEffect
虽然逻辑上 React 的处理是合理的,但是还是需要手动去解决它导致的性能问题:官方提供了 useCallback 这个 hooks,用于解决函数引用问题。

1
2
3
4
5
6
7
8
9
10
const App = () => {
const fetchData = useCallback(
(config: any) => {
queryList(config)
},
[queryList]
)

return <Child fetchData={fetchData} dataConfig={{ id: getId(queryId) }} />
}

但是这个时候还有一个地方没有解决——Props。只要 props 更新,组件还是会重新 fetchData,因为 dataConfig 也是一个会变化的 prop。memo 是一个最容易被忽略的 Hooks,即使我们有意不在 JSX 中做计算,写成这样:

1
2
3
4
5
6
7
8
9
10
const fetchData = useCallback(
(config: any) => {
queryList(config)
},
[queryList]
)

const dataConfig = queryConfig(id)

return <Child fetchData={fetchData} dataConfig={dataConfig} />

由于习惯函数式编程,我们已经习惯了这种写法。但是组件是有状态的,状态更新了就得处理相关逻辑,触发 re-render。我们需要告诉 React 什么时候该处理这个状态,这时候 useMemo 就登场了。

1
2
3
4
5
6
7
8
9
10
11
12
13
const fetchData = useCallback(
(config: any) => {
queryList(config)
},
[queryList]
)

const dataConfig = useMemo(
() => ({ ...config, id: getId(queryId) }),
[getId, queryId]
)

return <Child fetchData={fetchData} dataConfig={dataConfig} />

这样 dataConfig 只有在 getId 或 queryId 变化时才重新生成,组件才会在必要的时候重新 fetchData

memo

只使用 useMemo 和 useCallback 来进行优化是有可能达不到效果的,原因在于如果 props 引用不发生变化,虽然不会重新渲染,但它依然会重新执行。

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
const Child = ({ name }: Props) => {
console.log("run") // will log every second

// some complex calculations

return <div>{name}</div>
}

const App = () => {
const [name] = useState("foo")
const [count, setCount] = useState(0)

useEffect(() => {
setInterval(() => {
setCount((prev) => prev + 1)
}, 1000)
}, [])

return (
<div>
<div>{count}</div>
<Child name={name} />
</div>
)
}

如果 Child 中的计算量非常大,这时候的性能主要就耗在重新执行的这个过程了。如果想要阻断这一过程重新执行,React 有一个 API:memo,它相当于一个 PureComponent,是一个 HOC,默认对 props 进行一次浅比较,如果 props 不变,则不会重新执行。
现在给 Child 套上 memo:

1
2
3
4
5
6
7
const Child = memo(({ name }: Props) => {
console.log("run") // will log only once

// some complex calculations

return <div>{name}</div>
})

或者使用 useMemo 包裹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const App = () => {
const [name] = useState("foo")
const [count, setCount] = useState(0)

useEffect(() => {
setInterval(() => {
setCount((prev) => prev + 1)
}, 1000)
}, [])

const memoChild = useMemo(() => <Child name={name} />, [name])

return (
<div>
<div>{count}</div>
{memoChild}
</div>
)
}

何时何处使用?

  1. 开销大的组件可以考虑使用 memo。因为有的组件重新渲染的开销可能比用 memo 做浅比较的开销还小,但是如果组件的重新执行开销很大,使用 memo 一定可以加快性能。
  2. 用 useMemo 和 useCallback 来控制 props 的引用,和 memo 配套使用效果最佳。性能优化是一个整体的过程,不是单独在某个组件里就可以改善的。
  3. useMemo 避免昂贵计算,useCallback 解决 reference 问题,memo 解决 shouldComponentUpdate 问题。