在 React 中,下面几个概念容易搞混:
React.memoReact.useMemoReact.useCallback
在讲它们的使用场景和区别之前,有必要了解一个概念,叫做记忆化(Memoization) :
简单来讲,记忆化的意思就是缓存函数的计算结果,当下次使用相同的参数调用该函数时,直接返回缓存值而不需要执行这个函数了。或者缓存某个变量,当依赖项未发生变化时,直接返回原来的变量,否则将缓存设置为新变量。
弄清楚概念之后,我们再来看在 React 中为什么需要做记忆化。
案例
我们知道,在 React 组件中,如果 props 发生了变化,组件会自上而下重新渲染,它的子组件也会走重新渲染的流程,即使子组件的属性其实并没有发生任何变化。
例如我们在 Parent 组件中定义三个属性:
- 姓名(name):当用户在 input 框中输入姓名的时候,会实时更新 name,不传给子组件
- 年龄(age):传递给子组件,在子组件内更新年龄,点击后每次自增1
- 兴趣爱好(hobbies):传递给子组件,在子组件内更新兴趣爱好,点击后反转数组
如果我们只更新父组件中 name 属性的值,对子组件有何影响呢?具体可以分为以下四种场景:
父是类组件,子也是类组件
class ClassChild extends React.Component {
render() {
console.log('Child render')
return (
<div>
<button onClick={this.props.handleAgeClick}>{this.props.age}</button>
<button onClick={this.props.handleHobbiesClick}>{this.props.hobbies.join(',')}</button>
</div>
)
}
}
class ClassParent extends React.Component {
state = {
name: 'keliq',
age: 18,
hobbies: ['music', 'football'],
}
handleNameChange = evt => this.setState({ name: evt.target.value })
handleAgeClick = () => this.setState({ age: this.state.age + 1 })
handleHobbiesClick = () => this.setState({ hobbies: this.state.hobbies.reverse() })
render() {
console.log('Parent render')
return (
<div>
<input type="text" value={this.state.name} onChange={this.handleNameChange} />
<strong>{this.state.name}</strong>
<ClassChild
age={this.state.age}
hobbies={this.state.hobbies}
handleAgeClick={this.handleAgeClick}
handleHobbiesClick={this.handleHobbiesClick}
/>
</div>
)
}
}
可以看到,当我们即使我们传给 Child 子组件的属性其实并没有发生任何变化,依然重新渲染了。这个时候的优化方式为:将子组件改成继承自 React.PureComponent 即可。这个时候再修改 name,子组件是不会重新渲染的。
但是这个时候会带来另外一个问题,就是当我们点击子组件的 hobbies 按钮时,发现不会数组并没有反转。相信大部分同学已经知道原因了,就是 PureComponent 对 props 的比较是浅比较,我们用 arr.reverse() 的时候,其实并没有改变数组的引用,所以导致子组件未更新,解决方案就是:
// before
this.setState({ hobbies: this.state.hobbies.reverse() })
// after
this.setState({ hobbies: [...this.state.hobbies.reverse()] })
父是类组件,子是函数组件
function FunctionChild({ age, handleAgeClick, hobbies, handleHobbiesClick }) {
console.log('Child render')
return (
<div>
<button onClick={handleAgeClick}>{age}</button>
<button onClick={handleHobbiesClick}>{hobbies.join(',')}</button>
</div>
)
}
class ClassParent extends React.Component {
state = {
name: 'keliq',
age: 18,
hobbies: ['music', 'football'],
}
handleNameChange = evt => this.setState({ name: evt.target.value })
handleAgeClick = () => this.setState({ age: this.state.age + 1 })
handleHobbiesClick = () => this.setState({ hobbies: this.state.hobbies.reverse() })
render() {
console.log('Parent render')
return (
<div>
<input type="text" value={this.state.name} onChange={this.handleNameChange} />
<strong>{this.state.name}</strong>
<FunctionChild
age={this.state.age}
hobbies={this.state.hobbies}
handleAgeClick={this.handleAgeClick}
handleHobbiesClick={this.handleHobbiesClick}
/>
</div>
)
}
}
这个时候 React.memo 就派上用场了,我们对子组件进行包裹:
// 如果属性没有发生变化,则不重新渲染函数组件
const MemoFunctionChild = React.memo(FunctionChild)
然后在 Parent 组件的 render 函数里面,把 Child 换成 MemoChild 即可:
<MemoFunctionChild
age={this.state.age}
gender={this.state.gender}
handleAgeClick={this.handleAgeClick}
handleGenderClick={this.handleGenderClick}
/>
当再输入姓名的时候,就不会触发 Child 组件重新渲染了,也就是说用 React.memo 包裹函数组件和类组件中的 PureComponent 是非常类似的。但是也存在 arr.reverse() 不能更新的问题,因为本质上都是对 props 进行浅比较。
父是函数组件,子是类组件
class ClassChild extends React.Component {
render() {
console.log('Child render')
return (
<div>
<button onClick={this.props.handleAgeClick}>{this.props.age}</button>
<button onClick={this.props.handleHobbiesClick}>{this.props.hobbies.join(',')}</button>
</div>
)
}
}
function FunctionParent() {
console.log('Parent render')
const [name, setName] = React.useState('keliq')
const [age, setAge] = React.useState(18)
const [hobbies, setHobbies] = React.useState(['music', 'football'])
const handleNameChange = evt => setName(evt.target.value)
const handleAgeClick = () => setAge(age + 1)
const handleHobbiesClick = () => setHobbies(hobbies.reverse())
return (
<div>
<input type="text" value={name} onChange={handleNameChange} />
<strong>{name}</strong>
<ClassChild age={age} hobbies={hobbies} handleAgeClick={handleAgeClick} handleHobbiesClick={handleHobbiesClick} />
</div>
)
}
这个时候我们会发现,问题开始变得严重了,出现了两个问题:
- 即使子组件改成了 PureComponent,修改姓名的时候依然会触发子组件更新
- 点击子组件中的反转 hobbies 按钮没效果
我们先分析一下第一个问题:
函数组件每次渲染的时候都会重新执行函数,而 handleNameChange、handleAgeClick 和 handleHobbiesClick 是在函数内部创建的新变量,肯定每次都不一样,所以导致子组件使用 PureComponent 无效。
这个时候,React.useCallback 出场了!改成下面的代码之后,发现 name 改变再也不会触发子组件重新渲染了。
// before
const handleAgeClick = () => setAge(age + 1)
const handleHobbiesClick = () => setHobbies(hobbies.reverse())
// after
const handleAgeClick = React.useCallback(() => setAge(age + 1), [age])
const handleHobbiesClick = React.useCallback(() => setHobbies(hobbies.reverse()), [hobbies])
再看第二个问题,相信很多同学心里已经有答案了,原因就是 hobbies.reverse() 之后的引用没变,连父组件都没触发渲染,更别提子组件了。所以解决方案就是:
// before
const handleHobbiesClick = React.useCallback(() => setHobbies(hobbies.reverse()), [hobbies])
// after
const handleHobbiesClick = React.useCallback(() => setHobbies([...hobbies.reverse()]), [hobbies])
父是函数组件,子也是函数组件
function FunctionChild({ age, handleAgeClick, hobbies, handleHobbiesClick }) {
console.log('Child render')
return (
<div>
<button onClick={handleAgeClick}>{age}</button>
<button onClick={handleHobbiesClick}>{hobbies.join(',')}</button>
</div>
)
}
function FunctionParent() {
console.log('Parent render')
const [name, setName] = React.useState('keliq')
const [age, setAge] = React.useState(18)
const [hobbies, setHobbies] = React.useState(['music', 'football'])
const handleNameChange = evt => setName(evt.target.value)
const handleAgeClick = () => setAge(age + 1)
const handleHobbiesClick = () => setHobbies(hobbies.reverse())
return (
<div>
<input type="text" value={name} onChange={handleNameChange} />
<strong>{name}</strong>
<FunctionChild age={age} hobbies={hobbies} handleAgeClick={handleAgeClick} handleHobbiesClick={handleHobbiesClick} />
</div>
)
}
相信大家已经知道解法了,就是把 handleAgeClick 和 handleHobbiesClick 函数通过 React.useCallback 缓存起来,并且把子组件用 React.memo 包裹一下:
// before
const handleAgeClick = () => setAge(age + 1)
const handleHobbiesClick = () => setHobbies([...hobbies.reverse()])
<FunctionChild age={age} hobbies={hobbies} handleAgeClick={handleAgeClick} handleHobbiesClick={handleHobbiesClick} />
// after
const handleAgeClick = React.useCallback(() => setAge(age + 1), [age])
const handleHobbiesClick = React.useCallback(() => setHobbies([...hobbies.reverse()]), [hobbies])
<MemoFunctionChild age={age} hobbies={hobbies} handleAgeClick={handleAgeClick} handleHobbiesClick={handleHobbiesClick} />
不过你可能会问,为什么没有用到 React.useMemo 呢?别急,这就构造出可以使用 useMemo 的场景,我们把代码改造如下:
function FunctionChild({ data: { age, handleAgeClick, hobbies, handleHobbiesClick } }) {
console.log('Child render')
return (
<div>
<button onClick={handleAgeClick}>{age}</button>
<button onClick={handleHobbiesClick}>{hobbies.join(',')}</button>
</div>
)
}
function FunctionParent() {
console.log('Parent render')
const [name, setName] = React.useState('keliq')
const [age, setAge] = React.useState(18)
const [hobbies, setHobbies] = React.useState(['music', 'football'])
const handleNameChange = evt => setName(evt.target.value)
const handleAgeClick = () => setAge(age + 1)
const handleHobbiesClick = () => setHobbies([...hobbies.reverse()])
const data = {
age,
hobbies,
handleAgeClick,
handleHobbiesClick,
}
return (
<div>
<input type="text" value={name} onChange={handleNameChange} />
<strong>{name}</strong>
<FunctionChild data={data} />
</div>
)
}
区别就在于把之前传给 Child 的属性全部合并到 data 对象里面传过去了,这个时候如果想要父组件修改 name 的时候,不触发子组件更新,就需要 React.useMemo 出场了!
// before
const data = {
age,
hobbies,
handleAgeClick,
handleHobbiesClick,
}
<FunctionChild data={data} />
// after
const data = React.useMemo(
() => ({
age,
hobbies,
handleAgeClick,
handleHobbiesClick,
}),
[age, hobbies]
)
const MemoFunctionChild = React.memo(FunctionChild)
<MemoFunctionChild data={data} />
useMemo 其实就是帮助我们保存了一个全局变量(useCallback 是保存一个函数),当 age 和 hobbies 发生变化的时候,则更新这个变量,否则保持不变,从而实现给到 Child 组件的 props 不变的效果。
如果要再做一点优化的话,就是把点击事件用 useCallback 再包裹一下,因为没有必要每次进来都创建不同的 handleAgeClick 和 handleHobbiesClick 函数:
const handleAgeClick = React.useCallback(() => setAge(age + 1), [age])
const handleHobbiesClick = React.useCallback(() => setHobbies([...hobbies.reverse()]), [hobbies])
const data = React.useMemo(
() => ({
age,
hobbies,
handleAgeClick,
handleHobbiesClick,
}),
[age, handleAgeClick, handleHobbiesClick, hobbies]
)
总结
React.memo:作用于函数组件,确保 props 不变时(浅比较)不重新渲染,效果等同于类组件的 PureComponent。React.useMemo:在函数组件内部,用于缓存全局变量,当依赖项不变时,返回相同的值。React.useCallback:在函数组件内部,用于缓存全局函数,当依赖项不变时,返回相同的函数。