欢迎关注个人公众号睿Talk
,获取我最新的文章:javascript
对于新手来讲,没写过几回死循环的代码都很差意思说本身用过 React Hooks。本文将以useCallback
为切入点,谈谈几个 hook 的使用场景,以及性能优化的一些思考。java
这算是 Hooks 系列的第 3 篇,以前 2 篇的传送门:
React Hooks 解析(上):基础
React Hooks 解析(下):进阶react
先看一个最简单的例子:segmentfault
// 用于记录 getData 调用次数 let count = 0; function App() { const [val, setVal] = useState(""); function getData() { setTimeout(()=>{ setVal('new data '+count); count++; }, 500) } useEffect(()=>{ getData(); }, []); return ( <div>{val}</div> ); }
getData
模拟发起网络请求。在这种场景下,没有useCallback
什么事,组件自己是高内聚的。性能优化
若是涉及到组件通信,状况就不同了:网络
// 用于记录 getData 调用次数 let count = 0; function App() { const [val, setVal] = useState(""); function getData() { setTimeout(() => { setVal("new data " + count); count++; }, 500); } return <Child val={val} getData={getData} />; } function Child({val, getData}) { useEffect(() => { getData(); }, [getData]); return <div>{val}</div>; }
就这么轻轻松松,一个死循环就诞生了...闭包
先来分析下这段代码的用意,Child
组件是一个纯展现型组件,其业务逻辑都是经过外部传进来的,这种场景在实际开发中很常见。函数
再分析下代码的执行过程:性能
App
渲染Child
,将val
和getData
传进去Child
使用useEffect
获取数据。由于对getData
有依赖,因而将其加入依赖列表getData
执行时,调用setVal
,致使App
从新渲染App
从新渲染时生成新的getData
方法,传给Child
Child
发现getData
的引用变了,又会执行getData
若是明确getData
只会执行一次,最简单的方式固然是将其从依赖列表中删除。但若是装了 hook 的lint 插件,会提示:React Hook useEffect has a missing dependency
测试
useEffect(() => { getData(); }, []);
实际状况极可能是当getData
改变的时候,是须要从新获取数据的。这时就须要经过useCallback
来将引用固定住:
const getData = useCallback(() => { setTimeout(() => { setVal("new data " + count); count++; }, 500); }, []);
上面例子中getData
的引用永远不会变,由于他它的依赖列表是空。能够根据实际状况将依赖加进去,就能确保依赖不变的状况下,函数的引用保持不变。
假如在getData
中须要用到val
( useState 中的值),就须要将其加入依赖列表,这样的话又会致使每次getData
的引用都不同,死循环又出现了...
const getData = useCallback(() => { console.log(val); setTimeout(() => { setVal("new data " + count); count++; }, 500); }, [val]);
若是咱们但愿不管val
怎么变,getData
的引用都保持不变,同时又能取到val
最新的值,能够经过自定义 hook 实现。注意这里不能简单的把val
从依赖列表中去掉,不然getData
中的val
永远都只会是初始值(闭包原理)。
function useRefCallback(fn, dependencies) { const ref = useRef(fn); useEffect(() => { ref.current = fn; }, [fn, ...dependencies]); return useCallback(() => { const fn = ref.current; return fn(); }, [ref]); }
使用:
const getData = useRefCallback(() => { console.log(val); setTimeout(() => { setVal("new data " + count); count++; }, 500); }, [val]);
完整代码能够看这里。
通常会以为使用useCallback
的性能会比普通从新定义函数的性能好, 以下面例子:
function App() { const [val, setVal] = useState(""); const onChange = (evt) => { setVal(evt.target.value); }; return <input val={val} onChange={onChange} />; }
将onChange
改成:
const onChange = useCallback(evt => { setVal(evt.target.value); }, []);
实际性能会更差,能够在这里自行测试。究其缘由,上面的写法几乎等同于下面:
const temp = evt => { setVal(evt.target.value); }; const onChange = useCallback(temp, []);
能够看到onChange
的定义是省不了的,并且额外还要加上调用useCallback
产生的开销,性能怎么可能会更好?
真正有助于性能改善的,有 2 种场景:
定义
时须要进行大量运算, 这种场景极少useEffect
,又或者是配合React.Memo
使用:const Child = React.memo(function({val, onChange}) { console.log('render...'); return <input value={val} onChange={onChange} />; }); 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}/> </> ); }
上面的例子中,若是不用useCallback
, 任何一个输入框的变化都会致使另外一个输入框从新渲染。代码在这里。
本文深刻讲解了使用 hooks 过程当中死循环产生的缘由,并给出了解决方案。useCallback
并非提升性能的银弹,错误的使用反而会拔苗助长。