效能优化函数
本章节介绍的功能旨在改善应用程序中的效能。在不必要的情况下使用这些函数不仅会降低代码的可读性,也会增加维护的难度。
一般来说,若您的应用程序中没有效能问题,那就不要费心使用这些功能!(除了 useMemo()
之外,因为它有时候可以作为别种用途)。
memo()
memo()
是一个内建的 HOC,用于建立一个基于属性的可记忆版本的组件。简化版的 memo()
介面如下:
const memo = (
component: FunctionOrClass,
arePropsEqual?: CompareFunction
): Component => {
// ...
}
type CompareFunction<T> = (currentProps: T, nextProps: T) => boolean
// 使用 `memo()`
const Component = () => {
return (
// ...
)
}
const MemoizedComponent = memo(Component, () => {
// ...
})
memo()
的运作方式如下:
- 组件完成首次渲染后,React 会记住这次的渲染结果。
- 当组件因为父组件的重新渲染而随之重新渲染时,React 会使用
Object.is()
来检查 props 中每个属性的数值是否和前一次渲染相同。- 若没有任何属性发生变化,React 就会直接返回上次记住的渲染结果,不会运行组件中的任何代码。
- 否则组件就会照常重新渲染,并将先前的记忆值替换为新的渲染结果。
- 若您想要组件仅在特定的属性发生变化时才重新渲染,您可以传递一个函数给第二个参数
arePropsEqual()
来自订属性相等的检查逻辑。
因此,只有在被记忆的组件作为子组件时,memo()
的效果才得以显现。
何时该使用 memo()
?
通常 memo()
会用在较消耗资源的组件,并且某些属性会导致不必要的重新渲染的情况。这通常发生在父组件重新渲染较频繁,例如涉及拖拉功能 (drag and drop),或是子组件较庞大的情况,例如编辑器。
以下范例是如何使用 memo()
来解决我们在组件渲染中提到的一个问题:
import { memo, useState, useEffect } from 'react'
const Child = memo(() => {
useEffect(() => {
console.log('[Child] re-renders')
})
return <h1>I am child</h1>
})
export const Parent = () => {
const [count, setCount] = useState(0)
const increment = () => {
setCount(count + 1)
}
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>
Increment
</button>
<Child />
</div>
)
}
这种写法中的 Child
永远不会随着 Parent
一起重新渲染,因为这里的 arePropsEqual()
永远返回 true
。
假设某个组件被 memo()
记忆起来,这是否代表只要 arePropsEqual()
返回的是真值 (truthy),该组件就不会重新渲染?
不,并不是这样!我们知道组件会在响应式数值改变时重新渲染,但是属性并不是组件中唯一一个响应式数值。memo()
仅有在该次重新渲染是由父组件触发时才会作用,如果该次重新渲染是由非属性的响应式数值 (例如状态) 所导致的,那么组件依然会重新渲染。
您可以这样想:memo()
记忆的并不是组件输出的 HTML,也不是组件在某一个时刻的快照 (snapshot);相反地,他运作的方式比较像是指向某个特定组件实体的指标。当 arePropsEqual()
返回的为假值 (falsy) 时,新的组件实体会被产生,然后该指标就会从旧的实体转向新的实体。因此组件内部的变化依然会照常发生,不受 memo()
影响。
useMemo()
若您曾经学过 Vue,可以把 useMemo()
看成是不知道何时该更新自己的 computed()
。
useMemo()
是一个内建的钩子,用于记忆任何您想记忆的东西。与 useEffect()
相似,useMemo()
接收一个回呼函数 (calback function) 和一个依赖值阵列 作为参数。简化版的 useMemo()
介面如下:
type useMemo<T> = (
callback: () => T,
dependencies: any[],
) => void
// 使用 `useMemo()`
const something = useMemo(() => {
return ...
}, [])
useMemo()
的运作方式如下:
- React 在组件首次渲染时调用
callback
,并记住他的返回值。 - 当组件重新渲染时,React 会使用
Object.is()
来检查dependencies
中每个元素的值是否和前一次渲染相同。- 若没有任何元素发生变化,React 就会直接返回上次记住的数值。
- 否则
callback
就会被调用,并用他的返回值取代先前的记忆值。
何时该使用 useMemo()
?
通常 useMemo()
适用于以下情况:
- 在组件重新渲染时跳过较消耗资源的运算。
- 使变数在不同渲染间仍然能指向相同的记忆体位置。
- 当
useEffect()
和useState()
一起使用。
在组件重新渲染时跳过较消耗资源的运算
有时候我们需要在组件内运行较消耗资源的运算。若这些运算在每次渲染都被运行,我们可能就会在组件在重新渲染时感受到明显的延迟。然而,在 useMemo()
的帮助下,我们可以确保这些运算只会在某些数值发生变化时运行。例如:
import { useState, useMemo } from 'react'
export const Example = () => {
const [users, setUsers] = useState([
{ id: 1, name: 'User A' },
{ id: 2, name: 'User B' },
{ id: 3, name: 'User C' },
])
// 这会在每次渲染时运行。
const matchedUsers = users.filter(
(user) => user.name.includes('A')
)
// 这只会在 `users` 改变时运行。
const matchedUsers = useMemo(
() => users.filter((user) => user.name.includes('A')),
[users]
)
return (
// ...
)
}
使变数在不同渲染间仍然能指向相同的记忆体位置
有时候我们需要将某个非原始型别的数值 (例如函数) 当做子组件的属性。由于未被记忆的值会随着组件的重新渲染被重新声明,他们每次都会指向不同的物件,导致子组件上的 memo()
失效。
要解决这个问题,我们可以使用 useMemo()
来将数值记忆起来,这样我们就能在不同的渲染中取得相同的值。例如:
import { useMemo } from 'react'
export const Example = () => {
// 小心!
// 这会导致 `user` 在每次渲染中都指向不同的物件。
const user = {
age: 5,
}
// 这会使得 `user` 总是指向相同的物件。
const user = useMemo(() => ({
age: 5,
}), [])
return (
// ...
)
}
当 useEffect()
和 useState()
一起使用
有时候我们需要在某些属性或状态改变时更新另外一个状态。在某些情况下,使用 useMemo()
会比使用 useEffect()
+ setState()
还要理想。
长话短说,这种写法:
import { useState, useMemo } from 'react'
interface IExampleProps {
keyword: string
}
export const Example = ({ keyword }: IExampleProps) => {
const [users, setUsers] = useState([
{ id: 1, name: 'User A' },
{ id: 2, name: 'User B' },
{ id: 3, name: 'User C' },
])
const matchedUsers = useMemo(
() => users.filter((user) => user.name.includes(keyword)),
[keyword]
)
return (
// ...
)
}
会比下面这种写法还要简洁:
import { useState, useEffect } from 'react'
interface IExampleProps {
keyword: string
}
export const Example = ({ keyword }: IExampleProps) => {
const [users, setUsers] = useState([
{ id: 1, name: 'User A' },
{ id: 2, name: 'User B' },
{ id: 3, name: 'User C' },
])
const [matchedUsers, setMatchedUsers] = useState([])
useEffect(() => {
setMatchedUsers(
users.filter((user) => user.name.includes(keyword))
)
}, [keyword])
return (
// ...
)
}
我们可以使用 useMemo()
来记忆某个组件吗?
我们可以这么做!和 memo()
相似,若组件中任何非属性的响应式数值发生变化,被记忆的组件就会重新渲染。主要的差别是 memo()
会在 arePropsEqual()
的返回值为假值时建立新的组件实体,而 useMemo()
则会在 dependencies
发生变化时建立新的组件实体。
很重要的一点是,传入 useMemo()
的 callback
不该有副作用,例如修改变量或是调用 API。该函数应该要是纯净的,意即相同的输入总是会得到相同的输出,而且不会影响到其他的变量。
useCallback()
useCallback()
是一个内建的钩子,用于记忆一个函数。与 useEffect()
相似,useMemo()
接收一个回呼函数和一个依赖值阵列作为参数。简化版的 useCallback()
介面如下:
type useCallback<T extends Function> = (
callback: T,
dependencies: any[],
) => void
// 使用 `useCallback()`
const myFunction = useCallback(() => {
// ...
}, [])
useCallback()
的运作方式如下:
- 在组件首次渲染时,React 会记住
callback
。 - 当组件重新渲染时,React 会使用
Object.is()
来检查dependencies
中每个元素的值是否和前一次渲染相同。- 若没有任何元素发生变化,React 就会直接返回上次记住的数值。
- 否则旧的记忆值就会被新的
callback
取代。
举例来说:
import { useState, useCallback } from 'react'
export const Example = () => {
const [count, setCount] = useState(0)
const increment = () => {
setCount(count + 1)
}
const showCount = () => {
console.log(count)
}
const memoizedShowCount = useCallback(showCount, [])
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>
Increment
</button>
<button onClick={showCount}>
Show Count
</button>
<button onClick={memoizedShowCount}>
Show Count (Memoized)
</button>
</div>
)
}
在这个范例中,一开始点击 "Show Count" 和 "Show Count (Memoized)" 都会在主控台中显示 0
。在点击 "Increment" 三次后,点击 "Show Count" 显示了 3
,而点击 "Show Count (Memoized)" 却依然显示 0
。
发生这种情况是因为在首次渲染中,count
的数值为 0
,代表组件中所有的 count
都会被取代成 0
。我们并没有放置任何数值到 useCallback()
的依赖值阵列中,因此 memoizedShowCount()
中的 count
永远不会被更新,从而在调用的时候显示了 0
。
何时该使用 useCallback()
?
通常 useCallback()
使用在函数被作为子组件的属性,或是函数是某个副作用的依赖值的情况。举例来说:
import { memo, useState } from 'react'
const MemoizedChild = memo(() => {
// ...
})
export const Example = () => {
const [count, setCount] = useState(0)
const increment = () => {
setCount(count + 1)
}
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>
Increment
</button>
<MemoizedChild increment={increment} />
</div>
)
}
在这个范例中,尽管 MemoizedChild
已经用 memo()
记忆起来了,他还是会随着 Example
一同重新渲染。
这是因为每次 Example
重新渲染时,increment()
都会被重新声明;由于 increment()
属于非原始型别,他每次都会指向不同的物件,导致 memo()
认为 increment()
在两次渲染之间发生变化了。
要解决这个问题,我们可以将 increment()
包裹在 useCallback()
中,这样即使 Example
重新渲染,他也能指向相同的物件:
import { memo, useState, useCallback } from 'react'
const MemoizedChild = memo(() => {
// ...
})
export const Example = () => {
const [count, setCount] = useState(0)
const increment = useCallback(() => {
setCount(prev => prev + 1)
}, [])
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>
Increment
</button>
<MemoizedChild increment={increment} />
</div>
)
}
请注意,我们在 setCount()
中使用了更新函数,这样我们就不需要将 count
放在 useCallback()
的依赖值阵列中。如此一来我们就能保证被传递给 MemoizedChild
的 increment()
在每次渲染中都会指向相同的物件,从而使 memo()
能如预期的运作。
您可能已经注意到 useCallback()
和 useMemo()
非常相似,确实是如此!您也可以使用 useMemo()
来记忆一个函数,然而这可能会稍微降低代码的可读性。例如:
import { useMemo } from 'react'
// 这种写法有点难阅读。
const increment = useMemo(() => () => {
setCount(prev => prev + 1)
}, [])
// 这种写法比较好读,但是他的作用和 `useCallback()` 一模一样。
const increment = useMemo(() => {
return () => {
setCount(prev => prev + 1)
}
}, [])
虽然您可以藉由显性返回来让代码变得好看一些 (看起来其实挺不错的!),但是那样做的结果就和 useCallback()
一模一样。总而言之,只要将 useCallback()
视为返回 callback
本身,而不是返回 callback
调用的结果的 useMemo()
即可。
import { useMemo } from 'react'
const useCallback = (callback: () => any, dependencies: any[]) => {
return useMemo(
() => callback,
dependencies
)
}
我们不得不再说一次:尽量不要在不需要这些功能的地方使用他们!