react hooks: 什么时候该用 useMemo / useCallback
react hooks: 什么时候该用 useMemo / useCallback
背景
useMemo 返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
⬆️ 上面的话来自 React 官方文档
useMemo, useCallbck 是 react hooks 中用来缓存数据的重要手段,使用这两个 hook 可以一定程度上的优化组件的性能。
很多同学倾向于给每个变量都套上 useMemo / useCallback ,不过这种方式是不对的,因为这种优化方式是有成本的。
每个 useMemo / useCallback 都是天然的闭包, 里面的垃圾数据得不到及时的释放,不合理的使用会造成数据堆积,变成负优化。
什么时候该去使用 useMemo 捏
以下只代表个人见解,如有遗漏/错误,求指正!
当自身是引用类型 且要作为其他 hook 的依赖时
康康这个 demo
export const Component: React.FC = () => {
const [someDatas, setSomeDatas] = useState([1, 2, 3]);
const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
const datas100 = someDatas.map((item) => {
return item + 100;
});
const { bool } = otherData;
// Effect 1
useEffect(() => {
console.log('Effect1 : ', datas100);
}, [datas100]);
// Effect 2
useEffect(() => {
console.log('Effect2 : ', bool);
}, [bool]);
return (
<div>
<button
onClick={() => {
setSomeDatas((draft) => {
return [...draft, 1];
});
}}
>
update someDatas
</button>
<button
onClick={() => {
setOtherData((draft) => {
return { bool: !draft.bool };
});
}}
>
update otherDatas
</button>
</div>
);
};
他大概长这样:
如果点击 update someDatas , 会更新 someDatas,从而引起 datas100 数据的变化,从而导致 Effect1 的重新执行,这没问题:
如果点击 update otherDatas ,期望的是更新 otherDatas,从而引起 bool 数据的变化,从而引起 Effect2 的重新执行,但是……
在点击 update otherDatas 后,Effect1 和 Effect2 被同时执行了!
原因
hook 组件在每次 render 时都会重新执行整个函数体,那么 datas100 和 bool 这两个变量将被重新计算生成。
但 react 的 hook(useEffect, useMemo, useCallback 等) 会有一个 deps array(第二个参数),而这些 hook 要不要再次被计算取决于本次 deps array 中的数据与上次 deps array 中的数据进行一次浅 diff,如果不相等则再次运行这个 hook,反之则不执行。
我们都知道在 js 在存储变量时,简单来说会把引用类型的地址存在数据栈中,而将值存在数据堆中,而浅 diff 的原理是比较二者在栈中存储的数据是否相同,并不关心堆中的情况
由于在每次 render 中, datas100 都会被重新计算生成,因此每次 data100 在栈中存储的地址都是新分配的,即使在堆中的值相同,但是在 deps array 看来,当前状态和上一个状态的 datas100 就是不同的,因此每次 render 都会重新执行 Effect1。
虽然每次 render 中,bool 这个变量都会被重新获取,但因为 bool 是一个简单类型,值直接存在栈中,虽然每次都被重新生成,不过只要他们的值相同, deps array 就会认为他们是相等的,所以只会在 bool 的值本身改变后执行 Effect2
那如何去改变这种尴尬的情况呢?我们可以对上面的组件进行一次改进:
改进
export const Component: React.FC = () => {
const [someDatas, setSomeDatas] = useState([1, 2, 3]);
const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
const datas100 = useMemo(
() =>
someDatas.map((item) => {
return item + 100;
}),
[someDatas],
);
const { bool } = otherData;
// Effect 1
useEffect(() => {
console.log('Effect1 : ', datas100);
}, [datas100]);
// Effect 2
useEffect(() => {
console.log('Effect2 : ', bool);
}, [bool]);
return (
<div>
<button
onClick={() => {
setSomeDatas((draft) => {
return [...draft, 1];
});
}}
>
update someDatas
</button>
<button
onClick={() => {
setOtherData((draft) => {
return { bool: !draft.bool };
});
}}
>
update otherDatas
</button>
</div>
);
};
这次的改动是将获取 datas100 的逻辑包裹了 useMemo,这样只要 someDatas 不更新,datas100 就不会更新,那么 Effect1 将不会每次都执行,我们来点击一下 update otherDatas:
成功了,只有 Effect2 被执行了,我们再尝试点击 update someDatas:
爱了爱了,非常好用,Effect1 被正常执行了
因此我们可以得出结论1:当自身是引用类型,且要作为其他 hook 的依赖时,需要包裹 useMemo
当数据为引用类型,且要作为 props 传递给子组件时,推荐用 useMemo 包裹
假如我们把上文组件中的副作用(小声逼逼:假的副作用!,我说的是 useEffect !)抽离出来,把useMemo 去掉,我们会得到下面这个结构
interface Props {
datas100: number[];
bool: boolean;
}
export const ChildComponent: React.FC<Props> = ({ datas100, bool }) => {
// Effect 1
useEffect(() => {
console.log('Effect1 : ', datas100);
}, [datas100]);
// Effect 2
useEffect(() => {
console.log('Effect2 : ', bool);
}, [bool]);
return <div>我是子组件</div>;
};
export const Component: React.FC = () => {
const [someDatas, setSomeDatas] = useState([1, 2, 3]);
const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
const datas100 = someDatas.map((item) => {
return item + 100;
});
const { bool } = otherData;
return (
<div>
<button
onClick={() => {
setSomeDatas((draft) => {
return [...draft, 1];
});
}}
>
update someDatas
</button>
<button
onClick={() => {
setOtherData((draft) => {
return { bool: !draft.bool };
});
}}
>
update otherDatas
</button>
<ChildComponent datas100={datas100} bool={bool} />
</div>
);
};
此时我们点击 update otherDatas 后,再次裂开:
Effect1 又被错误的执行了,原因很简单,看了上文的大家已经懂了,这里不做过多阐述。
这个例子就是典型的,将一个引用类型传递给了子组件,但是没有做缓存。
如果子组件的某个 hook 以这个变量做了依赖,那这个 hook 也就失效了,他就像病毒一样,如果这个 hook 是 useMemo ,且他被传递给了更深一层的组件,那可想而知,后果很严重。
所以我通常给会传递给子组件的引用类型都加上 useMemo ,可能这个做法会让很多人反感,但毕竟业务代码不只有一个人维护,其他人也可能会更新这部分代码,如果他在子组件中使用了某个没有做缓存的引用类型的 props,那岂不原地裂开?
因此我们可以得出结论:
当数据为引用类型,且要作为 props 传递给子组件时,推荐用 useMemo 包裹
当处理数据的时间复杂度较高时,应当用 useMemo 包裹
我们来康康这个 demo:
interface Props {
datas: number[];
anyProps: any;
}
const Component: React.FC<Props> = ({ datas }) => {
const str = datas.sort((a, b) => a > b ? 1 : -1).join('->');
return <div>{str}</div>;
};
在这个 demo 中,假设 datas 的 length 非常大,那 datas.sort 的计算量会变得比较恐怖,每次 Component 更新(不是 datas 自身引起的更新)都会引起 str 被重新计算,这明显是不符合预期的,并且会非常卡顿。
因此我们可以给 str 也包裹上 useMemo:
const Component: React.FC<Props> = ({ datas }) => {
const str = useMemo(
() => datas.sort((a, b) => (a > b ? 1 : -1)).join('->'),
[datas],
);
return <div>{str}</div>;
};
如此改动下,当 这次 Component 的更新不是由 datas 引起的,str 就不会被重新计算,从而提高性能
当你不知道应不应该加 useMemo ,且它恰好是引用类型时
警告:最好不要这么做,除非你不熟悉 react, 手动滑稽🤪
可以想象一下,如果所有的引用类型都加了 useMemo,就绝不会出现 deps arr diff 混乱的问题,这种方式只能保证逻辑不会出错,但是性能就不一定了……
什么时候该去使用 useCallback 捏
以下只代表个人见解,如有遗漏/错误,求指正!
useCallback 其实是 memo 一个方法的 useMemo 的简写版本,他将 :
useMemo(() => {
return () => {
console.log("do something");
};
}, []);
简化成
useCallback(() => {
console.log("do something");
}, []);
那我们该如何判断一个 function 是否需要被包裹 useCallback 呢,其实我们只要遵循 上文中 useMemo 返回 引用类型时的使用原则即可:
- 当这个 function 作为其他 hook 的依赖时
- 当这个 function 作为子组件的 props 传递时
相信如果阅读了 useMemo 的部分,大家一定理解为什么要遵循这两条规则
By the way
有一个 case 是我还在纠结的一个点:当这个 function 被作为子组件 props 传递时,如果遇到 子组件仅使用这个 function 绑定某个 元素事件(onClick) 时,其实使用 useCallback 没什么意义,反而是一份额外的空间开销,但是在后续的产品迭代中,可能会出现这个 function 在子组件中被 hook 依赖的情况,那在这个情况下,我们很难会关注到他的父组件是否给这个 function 包裹了 useCallback,可能就会出现一些性能问题甚至是逻辑漏洞。
所以我还是倾向于 只要 function 被作为子组件的 props 时,就要包裹 useCallback。
最后偷偷 bibi 一下
以上内容仅代表我个人的理解,肯定会有一些说的不对的或者说的不全面的地方,所以要是有什么可以补充的,欢迎评论区留言 !!
转载自:https://juejin.cn/post/7052959877886378020