React Hooks(五) useCallback和useMemo

一 React.memo和useMemo

1.memo的作用

当父组件的数据变化时,代码会重新执行一遍,子组件数据没有变化也会执行,这个时候可以使用memo将子组件封装起来,让子组件的数据只在发生改变时才会执行。

我们先来看一个不用memo的例子:
改变count和num的值都会触发子组件的重新渲染

import React, {
    
     useEffect, useCallback, useState, useMemo } from 'react';
import Test4 from './Test4'

const Test2 = () => {
    
    
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(0)
  
  const changeCount = () => {
    
    
    setCount(count=>count+1)
  }

  const changeNum = () => {
    
    
    setNum(num=>num+1)
  }
  
  return (
   <>
   <div onClick={
    
    changeCount}>
    {
    
    count}
   </div>
   <div onClick={
    
    changeNum}>
     {
    
    num}
   </div>
   <Test4 num={
    
    num} />
   </>
  );

}

export default Test2;
import React, {
    
     useEffect, useRef, useState } from 'react';

const Test4 = (props) => {
    
    

    const {
    
    num} = props
    console.log('children-----')

    return (
     <div>
      {
    
    num}
     </div>
    );
  }
  
export default Test4;

如果我们用memo包裹子组件,就能控制子组件只在num发生变化时进行渲染。

export default React.memo(Test4);

但是这里有一个bug,如果在子组件上还自定义了函数,那么改变count的值依然能使子组件re-render。

import React, {
    
     useEffect, useCallback, useState, useMemo } from 'react';
import Test4 from './Test4'

const Test2 = () => {
    
    
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(0)
  
  const changeCount = () => {
    
    
    setCount(count=>count+1)
  }

  const changeNum = () => {
    
    
    setNum(num=>num+1)
  }

  const clickNum = () => {
    
    
    console.log('click-----')
  }

  return (
   <>
   <div onClick={
    
    changeCount}>
    {
    
    count}
   </div>
   <div onClick={
    
    changeNum}>
     {
    
    num}
   </div>
   <Test4 num={
    
    num} clickNum={
    
    clickNum} />
   </>
  );

}

export default Test2;
import React, {
    
     useEffect, useRef, useState } from 'react';

const Test4 = (props) => {
    
    

    const {
    
    num, clickNum} = props
    console.log('children-----')

    return (
     <div onClick={
    
    clickNum}>
      {
    
    num}
     </div>
    );
  }
  
export default React.memo(Test4);

主要原因就是当count值改变时,重新执行代码,clickNum空函数的地址会发生变化,从而导致子组件重新渲染。
总结:
memo的作用就相当于函数组件的PureComponent。
PureComponent会根据props和state的浅对比来实现shouldComponentUpate()。但是如果在PureComponent中包含了比较复杂的数据结构,可能会因为深层的数据不一致而产生错误的否定判断,导致界面无法更新。
memo也一样,如果函数组件被React.memo包裹,且其实现中拥有useState或useContext的Hook,当context发生变化时,它仍会重新渲染。且memo是浅比较,只要对象的内存地址不变,无论值怎么变化都不会触发render。

2.useMemo的作用

(1)解决因函数更新而渲染自己的问题,就可以使用useMemo,使用它将函数重新封装。
useMemo(fn,array)监听变量,第一个参数是函数,第二个参数是依赖,只有依赖变化时才会重新计算函数。
上面的例子,我们可以对clickNum函数做如下修改,只有num发生变化时才会触发函数的执行,才会触发子组件的re-render。

const clickNum = useMemo(() => {
    
    
    return ()=>{
    
    
      console.log('num changed')
    }
},[num])

这里也可以换成useCallback,效果是一样的:

const clickNum = useCallback(() => {
    
    
    console.log('num changed')
  },[num])

这样改变count的值就不会触发子组件的更新。
实际上通过useMemo或useCallback将函数包装一层后,会根据后面的依赖项决定是否返回一个新的函数,函数内部作用域也随之更新。
(2)如果组件中有复杂计算的函数,应该使用useMemo。因为useCallback缓存函数的引用,useMemo缓存计算数据的值。useMemo是避免在每次渲染时都进行高开销的计算的优化策略。

import React, {
    
     useEffect, useCallback, useState, useMemo } from 'react';
import Test4 from './Test4'

const Test2 = () => {
    
    
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(0)
  
  const changeCount = () => {
    
    
    setCount(count=>count+1)
  }

  const changeNum = () => {
    
    
    setNum(num=>num+1)
  }

  const doubleNum = useMemo(() => {
    
    
    return 2*num
  },[num])

  return (
   <>
   <div onClick={
    
    changeCount}>
    {
    
    count}
   </div>
   <div onClick={
    
    changeNum}>
     {
    
    num}
   </div>
   <div>{
    
    doubleNum}</div>
   </>
  );

}

export default Test2;

如果doubleNum是一个计算过程复杂或者开销较大的函数,可以用useMemo获取缓存的值,避免函数的多次执行。

二 useCallback

useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
官方的等式:useCallback(fn,deps) 相当于 useMemo(()=>fn,deps)。
我们可以将useCallback结合useEffect,实现按需加载。通过hooks的配合,使得函数不再仅仅是一个方法,而是可以作为一个值参与到应用的数据流中。

const Parent = () => {
    
    
  const [ count, setCount ] = useState(0);
  const [ step, setStep ] = useState(0);

  const fetchData = useCallback(() => {
    
    
    const url = "https://count=" + count + "&step=" + step;
  }, [count, step])

  return (
    <Child fetchData={
    
    fetchData} />
  )
}

const Child = (props) => {
    
    
  useEffect(() => {
    
    
    props.fetchData()
  }, [props.fetchData])

  return (
    // ...
  )
}

下面看一个useContext的频繁更新所带来的问题

const [text, setText] = useState('');

const handleSubmit = useCallback(() => {
    
    
  // ...
}, [text]);

return (
  <form>
    <input value={
    
    text} onChange={
    
    (e) => setText(e.target.value)} />
    <OtherForm onSubmit={
    
    handleSubmit} />
  </form>
);

text的频繁更新会带来handleSubmit的频繁执行,很耗性能。这里我们可以通过useRef进行优化。

const textRef = useRef('');
const [text, setText] = useState('');

const handleSubmit = useCallback(() => {
    
    
  console.log(textRef.current);
  // ...
}, [textRef]);

return (
  <form>
    <input value={
    
    text} onChange={
    
    (e) => {
    
    
      const {
    
     value } = e.target;
      setText(value)
      textRef.current = value;
    }} />
    <OtherForm onSubmit={
    
    handleSubmit} />
  </form>
);

useCallback的依赖是只比较值的, 如果是对象, 就是只比较引用。而textRef是一直存在不会销毁的跨生命周期对象, 引用一直不变, 相当于, useCallback的依赖为[]。使用 useRef 可以生成一个变量让其在组件每个生命周期内都能访问到,且 handleSubmit 并不会因为 text 的更新而更新,也就不会让 OtherForm 多次渲染。

三 关于useMemo和useCallback

1.共同点:
仅依赖数据发生变化, 才会重新计算结果,也就是起到缓存的作用。
2.区别:
(1)useMemo计算结果是return回来的值, 主要用于缓存计算结果的值,应用场景如:需要计算的状态
(2)useCallback计算结果是函数, 主要用于缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个state的变化会使整个组件都被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

猜你喜欢

转载自blog.csdn.net/LittleMoon_lyy/article/details/124542221
今日推荐