useMemo和useCallback使用场景

useMemo和useCallback使用场景

useMemo到底是做什么的,工作原理是什么。

简而言之,useMemo是用来缓存计算属性的。
计算属性其实是函数的返回值,或者说指那些以返回一个值为目标的函数。
有些函数,需要我们手动的去点击,去完成一些动作才触发。而有些函数,则是直接在渲染的时候就执行,在DOM区域被当作属性值一样去使用。
后者,就被称为计算属性。
而计算属性,最后一定会使用return返回一个值!

useMemo在什么情况下使用

第一种情况

当我们的某一个计算属性真的需要大量的计算时候

//Com组件
const Com = () => {
    
    //这种就是完全没必要被useMemo缓存的,计算过程一共也就一个创建变量,一个加一,缓存它反而亏本
     const computedFun1 = () => {
        let number = 0;
        number = numebr +1;
        return number;
    }

    //这个就需要缓存一下了,毕竟他每次计算的计算量还是蛮大的。
    const computedFun2 = () => {
        let number =  0;
        for(let i=0;i<100000;++i){
            number = number +i-(number-i*1.1);
        }
        return number;
    }
    
    return <div onClick = {handleFun1}>
        computed1:{computedFun1()}  //这个计算量小,是不需要使用useMemo缓存的,缓存它反而亏本
        computed2:{computedFun2()} //这个计算量大,需要缓存。
    </div>
}

在上面的示例中。

computedFun1这个函数是完全没必要去缓存的。计算量太小了。我们使用useMemo成本都比它计算的成本高。

而computedFun2这个函数是需要去缓存的,毕竟这个函数都计算量属实有点大了。缓存起来,能不执行就不执行。

第二种情况

当子组件依赖父组件的某一个依赖计算属性并且子组件使用了React.memo进行优化了的时候。
我想,能看到这里的同学一定不需要我太详细的讲解什么是React.memo。

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

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

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

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

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

React.memo检测的是props中数据的栈地址是否改变。而**父组件重新构建的时候,如果不缓存计算属性,那么计算属性将会被重新计算执行,并返回一个新的值(这意味这返回了一个新的存储地址),新的计算属性传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。
所以,在上面的代码示例里面,子组件是要被重新渲染的。

那么如何才能让子组件不进行重新渲染呢?useMemo第二种使用方法来了。

useMemo会在发现依赖没有变化之后返回旧的计算属性值。

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

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

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

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

这样,子组件就不会被重新渲染了。
代码示例一和代码示例二中的区别只有被传入的子组件的计算属性(toChildComputed函数)是否被useMemo保护。
我们只需要使用useMemo保护一下父组件中传入子组件的那个函数(toChildComputed函数)保证它不会在没有必要的情况下返回一个新的内存地址就好了。

useMemo是不是用的越多越好?

不是!!!

缓存,需要成本!
缓存并不是免费的,所有被useMemo保护的函数都会被加入useMemo的工作队列。

在组件进行渲染并且此组件内使用了useMemo之后,为了校验改组件内被useMemo保护的这个计算属性是否需要重新计算,它会先去useMemo的工作队列中找到这个函数,然后还需要去校验这个函数都依赖是否被更改。

这其中,寻找到需要校验的计算属性和进行校验这两个步骤都需要成本
当我们大量的使用useMemo之后,非但不能给项目带来性能上的优化,反而会为项目增加负担,我们将这种情况戏称为:反向优化。

总结:

useMemo是用来缓存计算属性的,它会在发现依赖未发生改变的情况下返回旧的计算属性值的地址。
useMemo绝不是用的越多越好,缓存这项技术本身也需要成本。
useMemo的使用场景之一是:只需要给拥有巨大计算量的计算属性缓存即可。
useMemo的另一个使用场景是:当有计算属性被传入子组件,并且子组件使用了react.memo进行了缓存的时候,为了避免子组件不必要的渲染时使用

useCallback
useCallback与useMemo差不多,前者用来返回函数,后者用来返回值。

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不但不能阻止函数重新构建还会增加“寻找指定函数和校验依赖是否改变”这两个功能,为项目增添不必要的负担。

你可能感兴趣的:(javascript,前端,react.js)