浅谈 React 常用的 Hooks
背景:
我认识的一些朋友在新学习 React 的时候,不太清楚useCallback有哪些使用场景,对于useCallback
、useEffect
的依赖处理也不是很熟练,使得 Hook
API 给他们带来了不少的心智负担。我根据我的经验,浅浅的讲讲常用的 Hook
的用法以及使用场景。如果文章有所不对的地方,请各位指点指点。
API 概述:
React 核心心智模型简单来讲就是UI=render(state)
,通过数据驱动视图。
React 有两种方式来创建 UI,分别为 class 组件和 函数组件。
在 React 16.8 之前,函数组件只能作为无状态组件(Stateless component
)来使用,函数组件顾名思义就是一个JavaScript/TypeScirpt
函数,通过这个函数实时计算返回 JSX,遵循「参数 => 返回值」这种「纯函数」的概念。
只要在 JavaScript/TypeScirpt
中执行或调用函数,会创建一个新的执行上下文并将其放在执行堆栈中。这样就创建了执行上下文,就像全局上下文一样。它将拥有自己的变量和函数空间。它将经历创建阶段,然后它将逐行执行函数中的代码。函数执行完毕之后,会弹出执行栈,如果发现没有变量再指向函数空间,也会在堆中销毁该函数空间。
由于函数组件调用一次之后,其内部的声明的变量会被销毁,而 React 组件反复执行函数创建 JSX,故不方便做状态管理。
于是 React 在 16.8 这个版本中,带来了 Hook API,可以在函数组件里面管理状态,让 React 更加的函数式。
useState
API 创建的 state 会被放在函数组件对应的 fiber
上面,通过 useState
返回的元组第二个参数setCount
触发更新,更新会造成组件 render
,组件 render
之后,useState
返回的count
为更新后的结果。
const [count,setCount] = useState()
useEffect
API有点像是将 class 中的 componentDidMount
、componentDidUpdate
、 componentWillUnmount
结合到了一起,但是也不能生搬硬套。
传给 useEffect
的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。useEffect
第二个参数为一个依赖数组,当依赖数组里面的值变化时,会重新执行useEffect
里面的函数,并且此时useEffect
里面才能拿到最新的依赖值。
有时你会在组件中添加一些事件订阅或者其他监听器,useEffect
返回了一个函数,将清理方法添加到此返回函数即可。
useEffect(()=>{
// do some effect
() => {
// clear some effect
}
},[deps])
Hook 提供了 useCallback
API,用来保证在函数组件里面声明的函数,在函数组件反复创建的过程中能指向同一个引用。该回调函数仅在某个依赖项(函数外部的 state、props 或者其他变量)改变时才会更新。
const handleClick = useCallback(()=>{
// do something
},[deps])
与之相对应的,useMemo
API 与 useCallback
很类似,useCallback
用来缓存函数,useMemo
用来缓存值,仅当依赖变化时才重新计算新的值,这种优化有助于避免在每次渲染时都进行高开销的计算。
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
const memoizedValue = useMemo(()=>{
// do something
},[deps])
API 详解
场景一:
我们写页面的时候,一定会遇到接口请求获取数据然后渲染的情况,比如我想查询全部用户信息,像这种副作用操作我们可以在useEffect
里面执行。
useEffect(()=>{
getAllUser().then((res)=>{
// do something
})
},[])
如果你想使用await
,可以用执行函数(IIFE)写法:
useEffect(()=>{
// Using an IIFE
(async function getUser() {
const users = await fetchAllUser();
// do something
})();
},[])
如果我们在详情页面需要根据 id 查询某一条用户的详细信息呢?我们需要将 id 放在依赖数组中,这样才能在useEffect
里面拿到最新的id值,而且为了避免页面挂载的时候,id 没有值的情况,需要先判断有无 id,再请求接口。
useEffect(()=>{
if(id){
fetchUser(id).then((res)=>{
// do something
})
}
},[id])
假如我们需要点击按钮之后,主动查询一次用户信息呢?为了复用性,我们需要将获取用户详情的逻辑抽离出来。
以下代码会符合预期的执行,但是存在一个问题:依赖不直观,若getUserInfo
中添加了依赖,忘记及时在useEffect
中更新依赖,则会出现不可预料的bug。
const getUserInfo = async () => {
if(id){
const user = await fetchUser(id)
// do something
}
}
useEffect(()=>{
getUserInfo()
},[id])
假如这样解决呢?这样确实会避免上面的问题,但是假如getUserInfo
里面需要再添加参数,需要修改useEffect
与getUserInfo
两个地方的代码,而且随着参数变多,两处代码的可维护性也相当的糟糕,useEffect
应该只做他该做的事情,保证代码的纯净性。
const getUserInfo = async (userID) => {
if(userID){
const user = await fetchUser(userID)
// do something
}
}
useEffect(()=>{
getUserInfo(id)
},[id])
可以用useCallback
来解决这个问题,如果getUserInfo
需要依赖更多的参数,只需要在此片段修改即可,依赖也很直观,不需要影响到useEffect
里面的代码。
当id数据更新时,useCallback
会重新返回一个新的函数,useCallback
里面的函数中会取到最新的id。作为useEffect
的依赖,既然getUserInfo
函数的引用更新了,那么useEffect
里面的函数也会重新执行。
const getUserInfo = useCallback(async () => {
if(id){
const user = await fetchUser(id)
// do something
}
},[id])
useEffect(()=>{
getUserInfo()
},[getUserInfo])
场景二:
从性能优化方面来讲,首先要考虑是怎么减少组件的render
。
React提供了memo
这个高阶组件(HOC),浅层检查 props 变更,跳过非必要的render,以提高组件的性能。
在函数组件中,由于memo
是浅层比较props的,而函数执行会反复创建新的上下文,函数里面的变量、函数都会生成新的引用,往往需要配合useCallback或者useMemo使用,才能达到最优的效果。
// Increase.tsx
export const Increase = ({ onClick }) => {
console.log("child render");
return <button onClick={onClick}>Increase</button>;
};
// App.tsx
function App() {
const [count, setCount] = useState(0);
const handleIncrease = () => {
setCount((c) => c + 1);
};
return (
<div>
<div>{count}</div>
<Increase onClick={handleIncrease} />
</div>
);
}
以上是很常见的场景,父组件将事件通过props
传递给子组件,以完成父子通信。每点击一次【Increase】按钮,count 数值会加1。
点击多次【Increase】按钮之后,控制台上会输出很多次的“child render”,这说明了子组件 Increase 在反复渲染,如何避免其反复渲染呢?
引入memo
试一下:
export const Increase = memo(({ onClick }) => {
console.log("child render");
return <button onClick={onClick}>Increase</button>;
});
看一看多次点击【Increase】按钮之后的结果
从控制台输出的内容来看,子组件仍反复创建,说明只添加memo并不会有效果。
改造一下父组件,在handleIncrease函数上面添加一个useCallback
:
function App() {
const [count, setCount] = useState(0);
const handleIncrease = useCallback(() => {
setCount((c) => c + 1);
}, []);
return (
<div>
<div>{count}</div>
<Increase onClick={handleIncrease} />
</div>
);
}
现在控制台只会在页面初始化的时候打印“child render”,无论点击多少次按钮,都不会再打印。这说明子组件不会再反复渲染了。
此时如果保留父组件函数中的useCallback
,而去掉子组件的
memo
,子组件仍会反复创建,只要是涉及到了事件的传递,要想达到阻止子组件没必要的重复渲染,这两者缺一不可。
结语:
以上简单的介绍了一下几个hook的常用的使用场景,后面有空会讲一讲函数组件中事件订阅与实时状态的坑。
转载自:https://juejin.cn/post/7196990328871911481