useCallback 带来的隐式依赖问题

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 新建文章组件
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);

哪些地方需要优化

  1. 子组件 ArticleTypeSetting 是使用 memo 包裹的,这个组件是希望尽可能的减少渲染次数的(假装这个组件有性能问题,一般不用包)。但是,现在每当修改任意一个值(如 other),子组件都会重新渲染,这显然是没有达到优化的预期的。
  2. 这里不规范, useEffect 中使用了 report 函数,但是没有将它放到依赖数组中。

对代码进行了一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取当前「标题」和「内容」的长度
  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 上来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

posted @   远方的少年🐬  阅读(753)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示