react-useCallback和useMemo

loading 2022年12月10日 69次浏览

1. 用法

首先两者的作用都是用于缓存。

两者的写法区别在于

        const handleChange = useCallback((e) => {
            setText(e.target.value)
        }, [])

        const handleChange1 = useMemo(() => (e) =>{
            setText(e.target.value)
        }, [])

useCallback不会自己执行第一个函数,而是将函数本身返回,也就是缓存函数。

比如某个组件只有当ajax函数变化时才会新创建一个effect,那么就可以把ajax封装成一个函数,并且用useCallback缓存下来。

const Component = () => {
    const ajax = React.useCallback(() => {
        // ajax代码
    }, [])
    React.useEffect(() => {
        ajax()
    }, [ajax])
    return (
        <div></div>
    )
}

因为函数式组件每次任何一个 state 的变化 整个组件都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

useMemo会直接执行第一个函数,然后将函数执行结果返回,也就是缓存结果值。

比如说通过特别复杂的逻辑计算出了一个结果,useMemo会直接缓存这个结果,只要不是依赖中的值改变了,就算其他的state/props改变了导致重新render,useMemo也会直接提供缓存好的值而不用重新计算那段复杂的逻辑。

    // 将过滤得到的数组返回给getCinemaList
    const getCinemaList = useMemo(() =>
        cinemaList.filter(item => item.name.toUpperCase().includes(mytext.toUpperCase()) ||
            item.address.toUpperCase().includes(mytext.toUpperCase())
        ),
        [cinemaList, mytext]
    )

2. 滥用后果

举个例子

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

当Com组件重新渲染时,被useCallBack包裹了的函数也会被重新构建并当成useCallBack函数的实参传入

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

注意区分函数创建函数执行,useCallBack直接返回旧函数地址可以使得新创建的fun1不会被执行。创建函数所消耗的性能和执行复杂函数所消耗的性能相比微不足道。

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

因此滥用useCallback同样会造成大量的性能浪费。

3. 使用方案

首先要弄清楚一个问题:发生什么事情时,组件会re-render?

  • 当本身的state或props改变
  • context的value改变,使用该值的组件会re-render
  • 当父组件re-render,子组件就会随着re-render

第三点是容易被忽视的,举个useCallback失效的例子:

const Component = () => {
    const [state, setState] = React.useState(1);

    const onClick = React.useCallback(() => {
        console.log('^click somthing^!')
    }, []);

    return (
        // 哪怕onClick使用了useCallback缓存优化,但是子组件仍会re-render
        <Child onClick={onClick} />
    )
}

该怎么让其生效呢?需要搭配React.memo使用

注意区分useMemo

React.memo是一个高阶组件,它接收一个组件作为参数,被memo包裹的组件,会在其父组件重新render时,对比传入子组件的props(浅比较,复杂对象只比较第一层),若props没有发生改变,则子组件就不会re-render。

比如这个例子,PageMemoized会在父组件重新render时,浅比较传入的onClick是否变化再决定PageMemoized组件是否需要re-render,但是onClick正好被useCallback缓存了,所以这里的子组件不会re-render。

const PageMemoized = React.memo(Page);

但是,如果PageMemoized组件从父组件不止接受了onClick一个prop,那么前面的优化就前功尽弃:

<PageMemoized onClick={onClick} value={[1, 2, 3]} />

这时候就需要用到useMemo去把value也缓存起来,就能实现子组件避免被随意re-render:

const value = useMemo(() => {
  return [1, 2, 3]
}, [])

总结:
当父组件没有向子组件传函数时,React.memo()就可以使子组件不重新渲染。
当父组件向子组件传递了函数属性,则memo需要搭配useCallBack使用

4. useCallback范例

当父组件向子组件传递函数时,且子组件被memo包裹时:

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

5. useMemo和React.memo

相同点:

  • 都可以用来缓存数据,避免子组件的无效重复渲染。

不同点:

  • React.memo是一个高阶组件,可以通过直接包裹住子组件来实现防止重复渲染。(除非父组件传函数过来就要搭配useCallBack)
  • useMemo是一个hook,使用useMemo返回的值,只会在useMemo的第二个依赖参数发生修改时才会发生修改。因此可以将useMemo返回的值传给子组件实现稳定性。