案例
// 新建文章组件
function EditArticle() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [other, setOther] = useState("");
// 获取当前「标题」和「内容」的长度
const getTextLen = () => {
return [title.length, content.length];
};
// 上报当前「标题」和「内容」的长度
const report = () => {
const [titleLen, contentLen] = getTextLen();
if (contentLen > 0) {
console.log(`埋点 >>> 标题长度 ${titleLen}, 内容长度 ${contentLen}`);
}
};
/**
* 副作用
* 当「标题」长度变化时,上报
*/
useEffect(() => {
report();
}, [title]);
return (
<div className="App">
文章标题 <input value={title} onChange={(e) => setTitle(e.target.value)} />
文章内容 <input value={content} onChange={(e) => setContent(e.target.value)} />
其他不相关状态: <input value={other} onChange={(e) => setOther(e.target.value)} />
<MemoArticleTypeSetting getTextLen={getTextLen} />
</div>
);
}
enum ArticleType {
WEB = "前端",
SERVER = "后端",
}
// 子组件,修改文章类型(无需关注,它只是接受了父组件的一个参数而已)
const ArticleTypeSetting: FC<{ getTextLen: () => number[] }> = ({ getTextLen }) => {
console.log(" --- ArticleTypeSetting 组件重新渲染 --- ");
const [articleType, setArticleType] = useState<ArticleType>(ArticleType.WEB);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setArticleType(e.target.value as ArticleType);
console.log( "埋点 >>> 切换类型,当前「标题」和「内容」长度:", getTextLen() );
};
return (
<div>
<div>文章类型组件,当选择类型时上报「标题」和「内容」长度</div>
<div>
{[ArticleType.WEB, ArticleType.SERVER].map((type) => (
<div>
<input type="radio" value={type} checked={articleType === type} onChange={handleChange} />
{type}
</div>
))}
</div>
</div>
);
};
const MemoArticleTypeSetting = memo(ArticleTypeSetting);
哪些地方需要优化
- 子组件 ArticleTypeSetting 是使用 memo 包裹的,这个组件是希望尽可能的减少渲染次数的(假装这个组件有性能问题,一般不用包)。但是,现在每当修改任意一个值(如 other),子组件都会重新渲染,这显然是没有达到优化的预期的。
- 这里不规范, useEffect 中使用了 report 函数,但是没有将它放到依赖数组中。
对代码进行了一些修改
// 获取当前「标题」和「内容」的长度
const getTextLen = useCallback(() => {
return [title.length, content.length];
}, [title, content]);
// 上报当前「标题」和「内容」的长度
const report = useCallback(() => {
const [titleLen, contentLen] = getTextLen();
if (contentLen > 0) {
console.log(`埋点 >>> 内容长度 ${titleLen}, 内容长度 ${contentLen}`);
}
}, [getTextLen]);
/**
* 副作用
* 当「标题」长度变化时,上报
*/
useEffect(() => {
report();
}, [title, report]);
还有问题
当 「文章内容」修改了之后,会触发 useEffect 继续上报
为什么
初衷只是使用 useCallback 避免频繁调用,但当一个 useCallback 的依赖项变化后,这个 useEffect 会被执行,就像上面修改过后的代码一样,「文章内容」修改了之后,也会触发 useEffect 的,这就是「useCallback 带来的隐式依赖问题」。
如何解决
将 函数绑定到 useRef 上来解决
const getTextLenRef = useRef<() => [number, number]>(() => [0, 0]);
// 获取当前「标题」和「内容」的长度
getTextLenRef.current = () => {
return [title.length, content.length];
};
// 上报当前「标题」和「内容」的长度
const report = () => {
const [titleLen, contentLen] = getTextLenRef.current();
if (contentLen > 0) {
console.log(`埋点 >>> 标题长度 ${titleLen}, 内容长度 ${contentLen}`);
}
};
/**
* 副作用
* 当「标题」长度变化时,上报
*/
useEffect(() => {
report();
}, [title]);
将函数绑定到 Ref上,ref 引用不论怎样都保持不变,而且函数每次 render ref 上又会绑定最新的函数,不会有闭包问题。我在开发一个复杂项目中,大量的使用了这种方式,这让我的开发效率提升。它让我专注于写业务,而不是专注于解决闭包问题。
思考我们最开始使用 useCallback 的理由
只有「需要保存一个函数闭包结果,如配合 debounce、throttle 使用」这个是真正需要使用 useCallback 的,其他的都可能带来风险
在绝大多数情况下,开发者想要的仅仅只是避免函数的引用变化而已,而 useCallback 带来的隐式依赖问题会给你带来很大的麻烦,所以推荐使用 useRefCallback,把函数挂到 ref 上,这样代码更不容易留下隐患或带来问题,还可以省去维护 useCallback 依赖项的精力
https://github.com/huyaocode/webKnowledge/issues/12
