useMemo钩子函数

238 阅读3分钟

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

对象问题:

子组件中接收从父组件中传过来的count

  function Child(props) {
            console.log("child render");
            return (
                <div>
                    <p>child:{props.state.count}</p> //找到对象里的值
                </div>
            )
        }
   const MemoChild = memo(Child)

父组件,点击按钮改变count

        function App() {
            console.log("father render");
            let initCount = {count:1} //初始对象
            const [state, setState] = useState(initCount) //使用对象
            return (
                <div>
                    <p>{count} father</p>
                    <MemoChild state={state} /> //传给子组件
                    <button onClick={() => {
                    initCount.count=initCount.count+1 //先加1
                     setCount(initCount) //再更新
                    }} >click</button>
                </div>
            )
        }

点击按钮,改变initCount的对象+1并且在setState更新initCount,打印结果,无论怎么点击使count+1都没有用,都一点都不会刷新。

但是当使用另一种方式+1,并且更新时,就可以更改状态:

        function App() {
            console.log("father render");
            const [state, setState] = useState({count:1})
            return (
                <div>
                    <p>{count} father</p>
                    <MemoChild state={state} />
        <button onClick={() => {setState({count:state.count+1})}}>click</button>
                </div>
            )
        }

为什么呢?通过观察两组代码可以知道,第二种方法是直接改变前一个对象,每次都重新覆盖前一个对象,也相当于每次都开辟新的内存空间,拥有一个新的内存地址,使memo检测到每次都有新的状态可以更新,所以这种方法是可以改变声明的。但第一种方法未改变对象的地址只改变值是不可以的(第一个例子),只是在同一个地址上改变了变量的值,本质对象地址还是没有改变。

跟随渲染:

场景,同样是在父组件中传递值到子组件,当改变父组件中与子组件无关的状态时,按理说memo的机制就会使子组件不会重新声明,但,如果子组件引用的是一个对象,每次当父组件重新声明时,对象都会跟着声明,那每次都会拥有新的地址,然后更新代码实现如下:

 function App() {
            console.log("father render");
            const [count, setCount] = useState(0)
            let data = {count:0} //设置一个对象传给子组件
            return (
                <div>
                    <p>{count} father</p>
                    <MemoChild data={data} />
                    <button onClick={() => {
                       setCount(count+1) //更改父组件中显示的count
                    }} >click</button>
                </div>
            )
        }

子组件中data状态并未改变:

 function Child(props) {
            console.log("child render");
            return (
                <div>
                    <p>child:{props.data.count}</p> //count始终是0
                </div>
            )
 }
        
  const MemoChild = memo(Child)

但由于每次父组件重新渲染,使data对象也重新开辟新的内存空间,导致子组件中count的值虽然不曾改变,却也跟随更新,由此可见,这种方式是有些浪费性能的。

函数计算

同样在子组件中显示一个经过计算的数据,且该数据也不会改变,只是不同的是,它是一个函数的返回值。

存在于父组件中,通过props把getDate的返回值传递给子组件
 let getData =()=>{
                let sum = 0;
                for (let i = 0; i < 10; i++) {
                    console.log("for");
                    sum+=1;
                }
                return {count:sum}
}

而在父组件中,只是进行点击按钮修改父组件中的count数据,对for中循环数据没有更改,但是,每一次改变父组件中数据,子组件中for都会重新执行循环,也会浪费性能.

image.png

此时钩子函数useMemo就出场了。

使用useMemo将for循环函数包裹起来,并且将依赖数据设置为count,只有当count改变时,才会重新执行for循环

  let getData =useMemo(()=>{
                let sum = 0;
                for (let i = 0; i < 10; i++) {
                    console.log("for");
                    sum+=1;
                }
                return {count:sum}
            },[count])

注意,改成钩子函数之后的形式就要将之前的调用getData方法改为只放上getData

 <MemoChild data={getData()} /> //useMemo之前
  <MemoChild data={getData} /> //useMemo之后

再去改变父组件中状态,也不会影响到子组件了。因为一直引用的都是同一个依赖返回值。

image.png

因为useMemo就相当于是做了一个函数返回值缓存,包裹之后传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值。

由此可知,被包裹后直接就是返回值,无需调用此函数,直接传递给子组件这个返回值即可。