欢迎关注我的公众号睿Talk
,获取我最新的文章:
一、前言
对于新手来说,没写过几次死循环的代码都不好意思说自己用过 React Hooks。本文将以useCallback
为切入点,谈谈几个 hook 的使用场景,以及性能优化的一些思考。
这算是 Hooks 系列的第 3 篇,之前 2 篇的传送门:
React Hooks 解析(上):基础
React Hooks 解析(下):进阶
二、useCallback 使用场景
先看一个最简单的例子:
// 用于记录 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
- 3 -> 5 是一个死循环
如果明确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
的引用永远不会变,因为他它的依赖列表是空。可以根据实际情况将依赖加进去,就能确保依赖不变的情况下,函数的引用保持不变。
三、useCallback 依赖 state
假如在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);
// 每次调用的时候,fn 都是一个全新的函数,函数中的变量有自己的作用域
// 当依赖改变的时候,传入的 fn 中的依赖值也会更新,这时更新 ref 的指向为新传入的 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
并不是提高性能的银弹,错误的使用反而会适得其反。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。