Bootstrap

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>
  )
}

改动的地方有两点:

  1. memo函数将子组件包裹起来。
  2. useCallback包裹handleOnChange函数。

在这里插入图片描述
可以发现,无论按钮点击多少次,文本框中输入怎样改变,子组件都只渲染了一次。

那么我们的目的也就达成了:useCallback 缓存了对应的函数,每次渲染时保证都是用的同一个。

但是值得注意的是:

  1. useCallback并不总是能够提高性能的,因为无论怎样,其包裹的内联函数总归是要创建的,并且还需要做一些对该内联函数监测的一些动作,若运用不恰当,反而会使性能下降。
  2. React.memoReact.useCallback 一定记得需要配对使用,缺了一个都可能导致性能不升反“降”。
  3. 因为无意义的浅比较也是要消耗那么一点点点的性能。

那么为什么需要配合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>
    )
  }
}

效果如下:
在这里插入图片描述
可以发现:

  1. 我们点击按钮,修改了parentMsg的状态,从页面效果来看也确实发生了变化。
  2. 我们传入给子组件中的props的内容是sonMsg,而我们点击按钮并未改变其值。
  3. 但是从控制台的输出可以发现,子组件同样随着父组件发生了渲染。

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)
}, [])

可以发现两者的区别:

  1. 接收的参数都是一样,都可以在依赖项发生变化后执行。
  2. 但是useMemo返回的是函数运行的结果
  3. 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

效果如下:

在这里插入图片描述
出现这种情况的原因分析:

  1. 我们可以发现useCallback的第二个参数传入的是一个空数组,其会根据数组里面的内容来判断是否需要重新渲染。
  2. 那么竟然传入的是空数组了,也就是说只会渲染一次,也就是说searchID拿到的值是第一次被创建的时候,也就是其默认值0。
  3. 这时候也就会形成一个闭包,无论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应该是一样的值。

而上述代码中:

  1. 依赖的searchID变了。
  2. 那么对应这个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

分析如下:

  1. useRef的作用是创造一个哪怕发生多次render也不会变化的引用。
  2. 我们将需要访问的值(searchID)更新到这个引用中,达到穿透闭包的功能。

效果如下:
在这里插入图片描述
毫无疑问的是,useCallback中的函数肯定是只创建了一次,毕竟我们第二个参数传入的是一个空数组,但是能够通过useRef引用来保证我每次获得的值都是最新的。

;