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
组件是一个纯展示型组件,其业务逻辑都是通过外部传进来的,这种场景在实际开发中很常见。
再分析下代码的执行过程:
App
渲染Child
,将val
和getData
传进去Child
使用useEffect
获取数据。因为对getData
有依赖,于是将其加入依赖列表getData
执行时,调用setData
,导致App
重新渲染,如果setData
前后设置的值引用相同,不会触发App
的重新渲染App
重新渲染时生成新的getData
方法,传给Child
Child
发现getData
的引用变了,又会执行getData
- 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
, 任何一个输入框的变化都会导致另一个输入框重新渲染。