useCallBack你真的知道怎么用吗。

10,414 阅读6分钟

本人已参与「新人创作礼」活动,一起开启掘金创作之路。

开题

文章主要说明的两点

useCallBack不是每个函数都需要使用!

useCallBack在什么情况下使用?

useCallBack不是每个函数都需要使用

看到这里,有些笔友就要发问三连了。

1.为什么不用useCallBack把每个函数都包一下呢?

2.useCallBack不是缓存工具吗?

3.将每个函数都缓存不是可以更好提升性能吗?

useCallBack是一个缓存工具没错。但实际上他并不能阻止函数都重现构建。

举个例子

//Com组件
const Com =  () => {

    //示例1包裹了useCallBack的函数
    const fun1 = useCallBack(() => {
        console.log('示例一函数');
        ...
    },[])
    
     //示例2没有包裹useCallBack的函数
    const fun2 = () => {
        console.log('示例二函数');
        ...
    }
    return <div></div>
}

大家看上方这种结构的组件,Com组件中包含了fun1和fun2两个函数。

是不是认为当Com组件重新渲染的时候,只有fun2(没有使用useCallBack的函数)函数会被重新构建,而fun1(使用了useCallBack的函数)函数不会被重新构建。

实际上,被useCallBack包裹了的函数也会被重新构建并当成useCallBack函数的实参传入。

useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack都无法阻止组件render时函数的重新创建!!

每一个被useCallBack的函数都将被加入useCallBack内部的管理队列。而当我们大量使用useCallBack的时候,管理队列中的函数会非常之多,任何一个使用了useCallBack的组件重新渲染的时候都需要去遍历useCallBack内部所有被管理的函数找到需要校验依赖是否改变的函数并进行校验。

在以上这个过程中,寻找指定函数需要性能,校验也需要性能。所以,滥用useCallBack不但不能阻止函数重新构建还会增加“寻找指定函数和校验依赖是否改变”这两个功能,为项目增添不必要的负担。

useCallBack在什么情况下使用?

在往子组件传入了一个函数并且子组件被React.momo缓存了的时候使用


像上一节所说的,useCallBack的作用不是阻止函数创建,而是在依赖不变的情况下返回旧函数地址(保持地址不变)。

React.memo(),是一种缓存技术。能看到这里的笔友我想都不需要我详细解释React.memo是干什么的。

简单说,React.memo()是通过校验props中的数据是否改变的来决定组件是否需要重新渲染的一种缓存技术,具体点说React.memo()其实是通过校验Props中的数据的内存地址是否改变来决定组件是否重新渲染组件的一种技术。

假设我们往子组件(假设子组件为Child组件)传入一个函数呢?当父组件的其他State与Child组件无关的state)改变的时候。那么,因为状态的改变,父组件需要重新渲染,那被React.memo保护的子组件(Child组件)是否会被重新构建?

就这个问题,举个栗子。有如下↓代码片段

代码示例一

import {useCallBack,memo} from 'react';
/**父组件**/
const Parent = () => {
    const [parentState,setParentState] = useState(0);  //父组件的state
    
    //需要传入子组件的函数
    const toChildFun = () => {
        console.log("需要传入子组件的函数");
        ...
    }
    
    return (<div>
          <Button onClick={() => setParentState(val => val+1)}>
              点击我改变父组件中与Child组件无关的state
          </Button>
          //将父组件的函数传入子组件
          <Child fun={toChildFun}></Child>
    <div>)
}

/**被memo保护的子组件**/
const Child = memo(() => {
    consolo.log("我被打印了就说明子组件重新构建了")
    return <div><div>
})

问:当我点击父组件中的Button改变父组件中的state。子组件会不会重新渲染。乍一看,改变的是parentState这个变量,和子组件半毛钱关系没有,子组件还被React.memo保护着,好像是不会被重新渲染。但这里的问题是,你要传个其他变量进去这也就走的通了。但是传入的是函数,不行,走不通。会重新渲染。

React.memo检测的是props中数据的栈地址是否改变。而父组件重新构建的时候,会重新构建父组件中的所有函数(旧函数销毁,新函数创建,等于更新了函数地址),新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。

所以,在上面的代码示例里面,子组件是要被重新渲染的。

那么如何才能让子组件不进行重新渲染呢?useCallBack的正确使用方法来了。

使用useCallBack包一下需要传入子组件的那个函数。那样的话,父组件重新渲染,子组件中的函数就会因为被useCallBack保护而返回旧的函数地址,子组件就不会检测成地址变化,也就不会重选渲染。

还是上面的代码示例,我们进行以下优化。

代码示例二

import {useCallBack,memo} from 'react';
/**父组件**/
const Parent = () => {
    const [parentState,setParentState] = useState(0);  //父组件的state
    
    //需要传入子组件的函数
    //只有这里和上一个示例不一样!!
    //只有这里和上一个示例不一样!!
    //只有这里和上一个示例不一样!!
    //只有这里和上一个示例不一样!!
    //只有这里和上一个示例不一样!!
    //只有这里和上一个示例不一样!!
    const toChildFun = useCallBack(() => {
        console.log("需要传入子组件的函数");
        ...
    },[])
    
    return (<div>
          <Button onClick={() => setParentState(val => val+1)}>
              点击我改变父组件中与Child组件无关的state
          </Button>
          //将父组件的函数传入子组件
          <Child fun={toChildFun}></Child>
    <div>)
}

/**被memo保护的子组件**/
const Child = memo(() => {
    consolo.log("我被打印了就说明子组件重新构建了")
    return <div><div>
})

这样,子组件就不会被重新渲染了。

代码示例一和代码示例二中的区别只有被传入的子组件的函数(toChildFun函数)是否被useCallBack保护。

我们只需要使用useCallBack保护一下父组件中传入子组件的那个函数(toChildFun函数)保证它不会在没有必要的情况下返回一个新的内存地址就好了。

总结

  • useCallBack不要每个函数都包一下,否则就会变成反向优化,useCallBack本身就是需要一定性能的
  • useCallBack并不能阻止函数重新创建,它只能通过依赖决定返回新的函数还是旧的函数,从而在依赖不变的情况下保证函数地址不变
  • useCallBack需要配合React.memo使用