useCallback钩子函数

149 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情

函数缓存

父组件中声明两个变量且没有赋任何值,初始会有一个自己的地址。

声明两个变量

let lastCount,lastValue //注意,没有赋值

场景:设置一个父组件,里面usestate两个状态,一个是count,另一个是value。

将两个数据显示在页面上,并设置两个方法,分别用来使count加一,和获取value的数据。将这两个方函数名称分别赋值给在最初let声明的两个变量,在运行函数的时候去判断函数名称是否与let声明的变量相等。

代码如下:

function App(){
    const [count,setCount] = useState(0)
    const [value,setValue] = useState("")
    let handleCount =()=>{setCount(count+1)}
    console.log(lastCount === handleCount ) //打印是否地址相同
    lastCount = handleCount //传递变量
    let handleValue=()=>{setValue(e.target.value)}
    console.log(lastValue === handleValue)
    lastValue = handleValue //传递变量
    return(
        显示count
        显示value
        点击按钮出发handleCount函数
        输入框onChange触发handleValue函数
    )
}

每次都先将函数值分别赋给变量,点击按钮或者输入监听改变后再打印赋之后的变量是否和声明的函数方法相等。

打印得知,两个输出语句都为false

image.png

因为函数组件中return每次执行都会重新声明,就会产生新的函数,会产生新的对象,也就是会产生新的堆存空间,新的地址,每一次let声明函数方法的地址都不可能与最初声明的lastCount,lastValue相等,所以打印的都是false。

下一个场景:

设置一个子组件,有一个按钮,将handleCount函数传给子组件,即子组件中点击按钮之后也进行count加一的操作

image.png

在父组件中输入框输入数字,即使子组件并未引用与value有关的数据和方法,但由于每次父组件的value改变导致父组件重新刷新,使得每次都let新的对象,生成新的地址,导致子组件中的count地址不断变化,即使数据没有发生改变也会重新渲染页面。这就是在浪费性能。

方案:使用useCallback

由于子组件中接受的为count数据,所以,可以设置为依赖count数据,当count不改变时,就不渲染子组件

image.png

将handleCount函数缓存起来,使其产生依赖,当数据未发生改变时,缓存函数就不会再重新声明,即每次执行同一个函数。

let handleCount = useMemo(()=>{
                    return ()=>{
                       setCount(count+1)
                    }
                },[count]
)

缓存起来,数据不变也并不代表这个函数不执行,每一次都会执行的,只不过执行的是同一个函数而已。

将handleCount函数缓存起来之后再测试前后对象的地址是否改变,打印可知,当子组件中有关数据未发生改变时,地址也不会改变的。

image.png

只有value改变时,子组件不声明,且前后地址相同。

如果没有依赖项,数组中为空,那么会始终缓存函数对象,打印的也始终未true,不会随着数据发生地址改变。


写到这里,可以察觉出useCallback与useMemo有些相似,因为它们都可以进行缓存,且都有依赖项可以设置。

回到上一篇的例子中,当传递一个对象到子组件中,每次改变父组件的数据而不改变子组件的数据时,不进行对象缓存也会导致每一次子组件都重新渲染。

情景重现:

 function Child(props) {
            console.log("child render");
            return (
                <div>
                    <p>child:{props.data.count}</p>
                </div>
            )
        }
 const MemoChild = memo(Child)
 function App() {
            console.log("father render");
            const [count, setCount] = useState(0)
            const [value, setValue] = useState('')
            let data = {count:0} 
            return (
                <div>
                    <p>{count} father</p>
                    <MemoChild data={data} />
                    <button onClick={() => {setCount(count+1)}} >click</button>
                     <input type="text"value={value} onChange={(e)=>{
                     setValue(e.target.value)}} />
                </div>
            )
        }

同样输入数据到输入框中,即使data数据没有改变,子组件也会重新声明,每次对象的地址都不相同,我们使用useMemo钩子函数将data对象包裹住,并依赖与count,使其灵活的声明。只不过当时注意一点要将data传入的方法改为变量的形式,因为useMemo缓存返回的是一个对象,直接就可以return拿出来用。

整合:

那是否也可以将useCallback中包裹的方法改为返回值的形式返回直接用呢?

测试一下:

原缓存函数:

let handleCount=useCallback(()=>{
        setCount(count+1)
    },[count])

现缓存对象:

let handleCount = useMemo(()=>{
          return ()=>{
              console.log("执行了吗?");
             setCount(count+1)
          }
      },[count])

更改成功,达到了与缓存函数一样的效果。

由此可见,可以将useCallback缓存函数的形式全部都改为useMemo缓存对象的形式。即useCallback 是useMemo的语法糖,能用useCallback都能用useMemo。