useCallBack
我们先来看看react官方文档的解释
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
复制代码
- useCallback钩子接收两个参数 内联回调函数 和 依赖数组。它将返回该回调函数的memoized函数。该回调函数仅在某个依赖项改变时才会更新。
- 当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
使用场景
可以先看看下代码
const Child = ({ count, fetchData }) => {
console.log("child render");
useEffect(() => {
fetchData();
}, [fetchData]);
return <>{count}</>;
};
function App() {
const [count, setCount] = useState(0);
//模拟api请求
const fetchData = () => {
setTimeout(() => {
setCount((old) => (old += 1));
}, 2000);
};
return <Child count={count} fetchData={fetchData} />;
}
复制代码
就这么轻轻松松,一个死循环就诞生了…。
上面的Child组件作为一个纯ui组件,其业务逻辑都是通过props传递。这种场景在日常开发中很常见。
先分析下代码的执行过程
- App渲染Child组件,将count,fetchData传递到Child组件。
- Child在useEffect钩子执行fetchData方法,因为对fetchData存在依赖,所以将fetchData添加到依赖项。
- fetchData执行时,会调用setCount,导致App组件重新渲染。
- App重新渲染,会生成新的fetchData传给Child组件。
- Child组件发现fetchData是一个新的引入,又会再次执行fetchData
- 这样 3 – 5这里就是一个死循环。
怎么办?
- 如果我们清楚知道fetchData只会执行一次的话,可以在Child组件useEffect中移除依赖项,这样可能会是最简单的办法,但是我们一般项目都在安装hook 的lint的插件,就会报错提示:React Hook useEffect has a missing dependency。
useEffect(()=>{
fetchData()
},[])
复制代码
此时useCallBack就可以派上用场
const fetchData = useCallback(() => {
setTimeout(() => {
setCount((old) => (old += 1));
}, 2000);
}, []);
复制代码
上面使用useCallback就可以保证fetchData函数的引用不会变化。
疑问
既然 useCallBack能够缓存函数,那岂不是将所有的react function组件里面使用到的函数都使用useCallBack来包装,就可以达到组件渲染优化的目的?
大家看了官方的解释说返回一个缓存后的函数,一般都会觉得使用了useCallBack性能会更好些。
我们看看下面例子
export default function App() {
const [value, setValue] = useState("");
const handelChange = (e) => {
setValue(e.target.value);
};
return <input type={value} onChange={handelChange} />;
}
复制代码
假如我们将handelChange改写成
const handelChange = useCallBack((e) => {
setValue(e.target.value);
},[]);
复制代码
因为上面其实等同于
const handelChange = (e)=>{
setValue(e.target.value);
}
const handelChangeMemoized = useCallBack(handelChange,[])
复制代码
在handelChange的基础开销下,还额外增加了useCallBack的开销,性能怎么可能会比单单handelChange还好呢。
那使用后性能还差了?先给出结论非也,
继续看以下例子
const Input1 = ({ value, handelChange }) => {
console.log("input 1 render");
return <input type={value} onChange={handelChange} />;
};
const Input2 = ({ value, handelChange }) => {
console.log("input 2 render");
return <input type={value} onChange={handelChange} />;
};
export default function App() {
const [value1, setValue1] = useState("");
const [value2, setValue2] = useState("");
const handelChange1 = (e) => {
setValue1(e.target.value);
};
const handelChange2 = (e) => {
setValue2(e.target.value);
};
console.log("app render");
return (
<>
<Input1 value={value1} handelChange={handelChange1} />
<br />
<Input2 value={value2} handelChange={handelChange2} />
</>
);
}
复制代码
上面的例子中,app渲染Input1和Input2组件,但是运行之后我们可以发现,无论我们改变value1还是value2 都会导致 app-render,input1-render,input2-render。从性能的角度下,我们改变 value1,input2组件是不应该渲染的。反之改变value2,input1组件也不该渲染。我们分析一下,是因为value1改变后,app重新渲染,导致handelChange2生成了一个新的引用,所以才导致input2组件渲染的。
这种情况下useCallback就可以使用了。
我们来改写上面的例子。使用useCallBack缓存change函数
const handelChange1 = useCallback((e) => {
setValue1(e.target.value);
}, []);
const handelChange2 = useCallback((e) => {
setValue2(e.target.value);
}, []);
复制代码
但是我们发现即使使用了useCallBack缓存change函数,修改value1的时候,input1和input2还是会render。这是为什么呢?
其实就是react渲染的基本逻辑,只要父组件更新必然会导致自组件的更新,即使子组件没有任何的props。
我们在app组件再添加一个child组件来验证下。
const Child = () => {
console.log("child render");
return <div />;
};
export default function App() {
const [value1, setValue1] = useState("");
const [value2, setValue2] = useState("");
const handelChange1 = useCallback((e) => {
setValue1(e.target.value);
}, []);
const handelChange2 = useCallback((e) => {
setValue2(e.target.value);
}, []);
console.log("app render");
return (
<>
<Input1 value={value1} handelChange={handelChange1} />
<br />
<Input2 value={value2} handelChange={handelChange2} />
<Child />
</>
);
}
复制代码
验证得出每一次修改value1或者value2都是console如下
- app render
- input1 render
- input2 render
- child render
app组件其实等同于
return React.createElement(React.Fragment, {}, [
React.createElement(Input1, {value:value1,handelChange:handelChange1}, null),
React.createElement(Input2, {value:value2,handelChange:handelChange2}, null),
React.createElement(Child, {}, null),
]);
复制代码
可以看出即使Child没有任何的props,但是最终编译出来开始会传递一个空对象作为props。这就是为什么Child也会更新的原因了。
如何避免这种渲染性能上的浪费:答案就是:class组件可以继承PureComponent,后者使用shouldComponentUpdate生命周期。Fucniton组件就是React.memo().
const Input1 = memo(({ value, handelChange }) => {
console.log("input 1 render");
return <input type={value} onChange={handelChange} />;
});
const Input2 = memo(({ value, handelChange }) => {
console.log("input 2 render");
return <input type={value} onChange={handelChange} />;
});
const Child = memo(() => {
console.log("child render");
return <div />;
});
export default function App() {
const [value1, setValue1] = useState("");
const [value2, setValue2] = useState("");
const handelChange1 = useCallback((e) => {
setValue1(e.target.value);
}, []);
const handelChange2 = useCallback((e) => {
setValue2(e.target.value);
}, []);
console.log("app render");
return (
<>
<Input1 value={value1} handelChange={handelChange1} />
<br />
<Input2 value={value2} handelChange={handelChange2} />
<Child />
</>
);
}
复制代码
这样就能达到渲染优化的效果。
useCallBack并不是万能,要结合实际情况,不然还是适得其反。