useCallback

1、使用场景

function App () {
  const [data, setData] = useState(null)

  function getData () {
    setTimeout(() => {
      setVal({ name: 'nassua' })
    }, 500)
  }

  useEffect(() => {
    getData()
  }, [])

  return (
    <div>{data?.name}</div>
  )
}

getData模拟发起网络请求。在这种场景下,没有useCallback什么事,组件本身是高内聚的。

如果涉及到组件通讯,情况就不一样了:

function Child ({ data, getData }) {
    useEffect(() => {
        getData()
    }, [getData])

    return (
        <div>{data?.name}</div>
    )   
}

function App () {
  const [data, setData] = useState(null)

  function getData () {
    setTimeout(() => {
      setVal({ name: 'nassua' })
    }, 500)
  }

  return (
    <Child data={data} getData={getData} />
  )
}

就这么轻轻松松,一个死循环就诞生了...

先来分析下这段代码的用意,Child组件是一个纯展示型组件,其业务逻辑都是通过外部传进来的,这种场景在实际开发中很常见。

再分析下代码的执行过程:

  1. App渲染Child,将valgetData传进去
  2. Child使用useEffect获取数据。因为对getData有依赖,于是将其加入依赖列表
  3. getData执行时,调用setData,导致App重新渲染,如果setData前后设置的值引用相同,不会触发App的重新渲染
  4. App重新渲染时生成新的getData方法,传给Child
  5. Child发现getData的引用变了,又会执行getData
  6. 3 -> 5 是一个死循环

如果明确getData只会执行一次,最简单的方式当然是将其从依赖列表中删除。但如果装了 hook 的lint 插件,会提示:React Hook useEffect has a missing dependency

实际情况很可能是当getData改变的时候,是需要重新获取数据的。这时就需要通过useCallback来将引用固定住:

function Child ({ data, getData }) {
    useEffect(() => {
        getData()
    }, [getData])

    return (
        <div>{data?.name}</div>
    )   
}

function App () {
  const [data, setData] = useState(null)

  const getData = useCallback(() => {
    setTimeout(() => {
      setVal({ name: 'nassua' })
    }, 500)
  }, [])

  return (
    <Child data={data} getData={getData} />
  )
}

2、依赖state

假如在getData中需要用到data( useState 中的值),就需要将其加入依赖列表,这样的话又会导致每次getData的引用都不一样,死循环又出现了...

React 使用 Object.is 来比较 state,对象是引用类型,所以不相等

const getData = useCallback(() => {
  console.log(data);
  setTimeout(() => {
    setData({ name: 'nassua' })
  }, 500)
}, [data])

如果我们希望无论data怎么变,getData的引用都保持不变,同时又能取到data最新的值,可以通过自定义 hook 实现。注意这里不能简单的把data从依赖列表中去掉,否则getData中的data永远都只会是初始值(闭包原理)。

function useRefCallback(fn, dependencies) {
  const ref = useRef(fn)

  // 每次调用的时候,fn 都是一个全新的函数,函数中的变量有自己的作用域
  // 当依赖改变的时候,传入的 fn 中的依赖值也会更新,这时更新 ref 的指向为新传入的 fn
  useEffect(() => {
    ref.current = fn
  }, [fn, ...dependencies])

  return useCallback(() => {
    const fn = ref.current
    return fn()
  }, [ref])
}

使用:

const getData = useRefCallback(() => {
  console.log(data)
  setTimeout(() => {
    setVal({ name: 'nassua' })
  }, 500)
}, [data])

3、性能

一般会觉得使用useCallback的性能会比普通重新定义函数的性能好, 如下面例子:

function App() {
  const [val, setVal] = useState('')

  const onChange = (e) => {
    setVal(e.target.value)
  }

  return <input val={val} onChange={onChange} />
}

onChange改为:

const onChange = useCallback(e => {
  setVal(e.target.value)
}, [])

 实际性能会更差,可以在这里自行测试。究其原因,上面的写法几乎等同于下面:

const temp = e => {
  setVal(e.target.value)
}

const onChange = useCallback(temp, [])

可以看到onChange的定义是省不了的,而且额外还要加上调用useCallback产生的开销,性能怎么可能会更好?

真正有助于性能改善的,有 2 种场景:

  • 函数定义时需要进行大量运算, 这种场景极少
  • 需要比较引用的场景,如上文提到的useEffect,又或者是配合React.Memo使用:
function App() {
  const [val1, setVal1] = useState('')
  const [val2, setVal2] = useState('')

  const onChange1 = useCallback( evt => {
      setVal1(evt.target.value)
  }, [])

  const onChange2 = useCallback( evt => {
      setVal2(evt.target.value)
  }, [])

  return (
      <>
        <Child val={val1} onChange={onChange1}/>
        <Child val={val2} onChange={onChange2}/>
      </>
  )
}

const Child = React.memo(function({val, onChange}) {
  console.log('render...')
  
  return <input value={val} onChange={onChange} />
})

上面的例子中,如果不用useCallback, 任何一个输入框的变化都会导致另一个输入框重新渲染。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值