useMemo和useCallback以及其闭包性
一. useMemo和useCallback
1.1 useMemo和useCallback的使用
首先,来讲一下这两个函数是用来干嘛的:
useMemo
可以保证依赖的值未发生改变的时候,不触发值改变。useCallback
可以保证依赖的值未发生改变的时候,不触发函数引用的改变。
案例1:
1.自己去通过脚手架命令创建个React项目:
create-react-app react-test
2.入口文件index.js
:
// 引入React核心库
import React from 'react'
// 引入ReactDOM
import ReactDOM from 'react-dom'
// 引入App组件
import App from './App'
// 渲染App到页面
ReactDOM.render(<App />, document.getElementById('root'))
3.父组件App.js
:
import React, { Component } from 'react'
import Test from './Test'
export default class App extends Component {
render() {
return (
<div>
<Test/>
</div>
)
}
}
4.我们自定义的组件Test
(文件名为Test.js
):
import React, { useState, memo, useMemo, useCallback } from 'react'
const Child = (props) => {
console.log('子组件发生一次渲染:' + props);
return (
<div>
<input type="text" onChange={props.onChange} />
</div>
)
}
const Parent = () => {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
const handleOnChange = (e) => {
setText(e.target.value)
}
return (
<div>
<div>count: {count}</div>
<div>text: {text}</div>
<button onClick={() => {
setCount(count + 1)
}}>+1</button>
<Child onChange={handleOnChange} />
</div>
)
}
function Test() {
return <div><Parent /></div>
}
export default Test
实现的效果如下:
可以发现,在父组件中的任何能够引起重新渲染的操作,都会引起子组件重新渲染。这肯定不是我们希望看到的结果:父组件改变了,子组件又没变,凭啥要子组件也要跟着渲染?
这时候useCallback
的作用就来了,代码修改如下:
const Child = memo((props) => {
console.log('子组件发生一次渲染:' + props);
return (
<div>
<input type="text" onChange={props.onChange} />
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
const handleOnChange = useCallback((e) => {
setText(e.target.value)
}, [])
return (
<div>
<div>count: {count}</div>
<div>text: {text}</div>
<button onClick={() => {
setCount(count + 1)
}}>+1</button>
<Child onChange={handleOnChange} />
</div>
)
}
改动的地方有两点:
- 用
memo
函数将子组件包裹起来。 - 用
useCallback
包裹handleOnChange
函数。
可以发现,无论按钮点击多少次,文本框中输入怎样改变,子组件都只渲染了一次。
那么我们的目的也就达成了:useCallback
缓存了对应的函数,每次渲染时保证都是用的同一个。
但是值得注意的是:
useCallback
并不总是能够提高性能的,因为无论怎样,其包裹的内联函数总归是要创建的,并且还需要做一些对该内联函数监测的一些动作,若运用不恰当,反而会使性能下降。React.memo
和React.useCallback
一定记得需要配对使用,缺了一个都可能导致性能不升反“降”。- 因为无意义的浅比较也是要消耗那么一点点点的性能。
那么为什么需要配合React.memo
来使用呢?我们来看下第二小节。
1.2 memo和浅比较
首先从最原始的例子和地方开始讲起,我们知道,组件是否需要重新渲染,我们可以通过一个钩子函数来进行判断:shouldComponentUpdate
,其返回值为true
/false
。案例如下:
父组件App.js
:
import React, { Component } from 'react'
import { Button } from 'antd'
import Son from './Son'
export default class App extends Component {
state = {
parentMsg: 'parent',
sonMsg: 'son'
}
render() {
return (
<div >
{/* 修改parentMsg的状态,sonMsg的状态则不发生变化 */}
<Button onClick={() => { this.setState({ parentMsg: 'parent,' + '更改时间:' + Date.now() }) }}>
修改parentMsg值
</Button><br />
父组件信息:<br />
{this.state.parentMsg} <br />
子组件信息:<Son sonMsg={this.state.sonMsg} />
</div>
);
}
}
子组件Son.js
:
import React, { Component } from 'react';
export default class Son extends Component {
render() {
console.log('子组件发生渲染 ,' + '更改时间:' + Date.now());
const msg = this.props.sonMsg;
return (
<div> {msg}</div>
)
}
}
效果如下:
可以发现:
- 我们点击按钮,修改了
parentMsg
的状态,从页面效果来看也确实发生了变化。 - 我们传入给子组件中的
props
的内容是sonMsg
,而我们点击按钮并未改变其值。 - 但是从控制台的输出可以发现,子组件同样随着父组件发生了渲染。
1.2.1 shouldComponentUpdate
显然,这并不是我们预想到的结果,要想改变这种情况,可以通过钩子函数shouldComponentUpdate
来改变,在子组件中添加如下代码:
shouldComponentUpdate(nextProps, nextState) {
// 当 当前值和预期值一样的时候,返回false。此时会进行拦截,子组件不会渲染
return this.props.sonMsg !== nextProps.sonMsg
}
效果如下:
1.2.2 memo
shouldComponentUpdate
是用于类式组件的,那么对于函数式组件又该怎么办呢?
解决:React.memo
。
像上述情况,如果函数组件中给定相同的props
的情况下,需要渲染相同的结果,那么就可以通过将其包装在 React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能。即React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
子组件:
import React, { memo } from 'react';
const Son = memo((props) => {
console.log('子组件发生渲染 ,' + '更改时间:' + Date.now());
return (
<div> {props.val}</div>
)
})
export default Son;
1.3 useMemo和useCallback的区别
用useMemo
来代替useCallback
:
useMemo
版本:
const handle = (e) => {
setText(e.target.value)
}
const handleOnChange = useMemo(() => handle, [])
useCallback
版本:
const handleOnChange = useCallback((e) => {
setText(e.target.value)
}, [])
可以发现两者的区别:
- 接收的参数都是一样,都可以在依赖项发生变化后执行。
- 但是
useMemo
返回的是函数运行的结果。 useCallback
返回的是函数。
useCallback(fn,deps)
相当于useMemo(()=> fn,deps)
。
二. useCallback的闭包问题
案例2:
修改我们自定义的组件Test
(文件名为Test.js
):
import React, { useState, memo, useRef, useCallback } from 'react'
import { Button } from 'antd'
const Test = () => {
const [searchID, setSearchID] = useState(0)
const query = useCallback(() => {
console.log('searchID的值是:' + searchID)
}, [])
return (
<>
<div>当前的值为:{searchID}</div>
<Button onClick={() => { setSearchID(searchID + 1) }} >加1</Button>
<Button onClick={() => { query() }}>发送请求</Button>
</>
)
}
export default Test
效果如下:
出现这种情况的原因分析:
- 我们可以发现
useCallback
的第二个参数传入的是一个空数组,其会根据数组里面的内容来判断是否需要重新渲染。 - 那么竟然传入的是空数组了,也就是说只会渲染一次,也就是说
searchID
拿到的值是第一次被创建的时候,也就是其默认值0。 这时候也就会形成一个闭包
,无论searchID
再怎么改变,useCallback
第一次创建的内部函数中的searchID
已经是死的了。
闭包的概念:
外部函数定义的内部函数就是闭包,并有权访问另一个函数作用域中的变量。
大家可能看到这里会觉得,那我在数组中加一个searchID
不就好啦?如下:
const query = useCallback(() => {
console.log('searchID的值是:' + searchID)
}, [searchID])
如果这么加了,点击发送请求
按钮,的的确确发出的值是实时更新的,但是这也就失去了useCallback
原本的意义。
useCallback
的真正目是在于缓存了每次渲染时 inline callback
的实例。
如果说:callback
依赖于一个经常变化的state
(如上面代码),那么该callback
的引用是无法缓存的。因为callback
内部对state的访问依赖于JS函数的闭包,如果希望callback
不变,那么每次访问的时候,之前那个callback
函数闭包中的state
应该是一样的值。
而上述代码中:
- 依赖的
searchID
变了。 - 那么对应这个
callback
也会随之改变,重新创建。
2.1 解决闭包
先来看下代码:
import React, { useState, useRef, useCallback } from 'react'
import { Button } from 'antd'
const Test = () => {
const [searchID, setSearchID] = useState(0)
const textRef = useRef();
textRef.current = searchID; // 将 searchID 写入到 ref
const query = useCallback(() => {
let param = textRef.current
console.log('searchID的值是:' + param)
}, [])
return (
<>
<div>当前的值为:{searchID}</div>
<Button onClick={() => { setSearchID(searchID + 1) }} >加1</Button>
<Button onClick={() => { query() }}>发送请求</Button>
</>
)
}
export default Test
分析如下:
useRef
的作用是创造一个哪怕发生多次render
也不会变化的引用。- 我们将需要访问的值(
searchID
)更新到这个引用中,达到穿透闭包的功能。
效果如下:
毫无疑问的是,useCallback
中的函数肯定是只创建了一次,毕竟我们第二个参数传入的是一个空数组,但是能够通过useRef
引用来保证我每次获得的值都是最新的。