细说useCallback问题和隐患并给出解决方案

点上方蓝字关注公众号「前端技术江湖」

本文来自作者:胡耀的投稿,原文地址:https://github.com/huyaocode/webKnowledge/issues/12

官网对 useCallback 的介绍

返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

使用 useCallback 的场景?

  1. 函数被 useEffect 内部所使用,但为了避免频繁 useEffect 的频繁调用,所以我包一下;

  2. 我只是为了解决 hooks lint 的提示报警,所以我包一下;

  3. 因为有一个使用了 useCallback 的函数引用了我这个函数,所以我不得不包一下;

  4. 当这个函数会被传递给子组件,为了避免子组件频繁渲染,使用 useCallback 包裹,保持引用不变;

  5. 需要保存一个函数闭包结果,如配合 debounce、throttle 使用;

  6. 我希望这个useCallback包裹但函数,但某个依赖项变化时,引用了我这个函数的所有 useEffect 都得重新执行一下;我们做了投票,发现场景 4 是使用的最多的

案例

假如这里有个文章组件,我想观察当「文章内容」明确后, 用户对「文章标题」的修改频率如何。这个
具体实现:当「文章内容」的长度大于 0 时编辑「文章标题」就上报埋点,同时带上「文章标题」和「文章内容」的 字符长度。

点击链接,打开 console 看Demo:https://codepen.io/huyao/pen/LYbdomv?editors=0010

小胡写出了下面这一段代码,大家可以细看一下,有哪些地方需要优化?它有哪些地方不规范?

// 新建文章组件
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(`埋点 >>> O长度 ${titleLen}, kr长度${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);

注:这段代码是我为了模拟 useEffect 和 useCallback 同时使用而构思出来的,大家不用深究。当然有其他写法可以避免问题,我们这里只是借这个 Demo 描述一下这种类型的场景。

CodeReview 与修改

哪些地方需要优化?

子组件 ArticleTypeSetting 是使用 memo 包裹的,这个组件是希望尽可能的减少渲染次数的(假装这个组件有性能问题,一般不用包)。但是,现在每当修改任意一个值(如 other),子组件都会重新渲染,这显然是没有达到优化的预期的。

哪些地方不规范?

这里不规范, useEffect 中使用了 report 函数,但是没有将它放到依赖数组中。我认为这是一件比较危险的事情,在 Hooks 中经常有过期状态的问题。插件已经帮你提示了,虽然现在你自测感觉没问题,但你很难保证在经过几轮轮修改之后,虽然你的代码一堆 warning 或 error,但跑起来没问题。

修改代码

小胡于是对代码进行了一些修改:

  1. 将 getTextLen 和 report 使用 useCallback 包裹

 // 获取当前「标题」和「内容」的长度
  const getTextLen = useCallback(() => {
    return [title.length, content.length];
  }, [title, content]);


  // 上报当前「标题」和「内容」的长度
  const report = useCallback(() => {
    const [titleLen, contentLen] = getTextLen();
    if (contentLen > 0) {
      console.log(`埋点 >>> O长度 ${titleLen}, kr长度${contentLen}`);
    }
  }, [getTextLen]);


  /**
   * 副作用
   * 当「标题」长度变化时,上报
   */
  useEffect(() => {
    report();
  }, [title, report]);
还有问题吗?

有,当 「文章内容」修改了之后,会触发 useEffect 继续上报,这个问题比较隐晦,不再回归测试的话难以发现;并且编辑文章内容时子组件也在重新渲染。

为什么出了问题?

我的初衷只是使用 useCallback 避免频繁调用,但当一个 useCallback 的依赖项变化后,这个 useEffect 会被执行,就像上面修改过后的代码一样,「文章内容」修改了之后,也会触发 useEffect 的,这就是「useCallback 带来的隐式依赖问题」。

如何解决?

方式 1(不推荐):将所有状态都挂到 Ref 上,然后每次修改状态之后主动触发渲染

这种做法确实可以解决这个问题,但是代码不宜维护,理由如下:

  1. 一旦一个组件这样写了之后,之后要有什么新的状态也只好放到这里面。
    而在新写组件的时候,你不知道什么时候会碰到这个问题,因为一旦碰到了你只有使用 forceUpdate 来解决,要改相关状态的定义,每次使用的时候还要把 someSate 改为 ref.someState。

  2. 每次想更新视图时都需要 forceUpdate,官网是不推荐这种方式的,链接点我

方式 2:将 函数绑定到 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(`埋点 >>> O长度 ${titleLen}, kr长度${contentLen}`);
    }
  };


  /**
   * 副作用
   * 当「标题」长度变化时,上报
   */
  useEffect(() => {
    report();
  }, [title]);

将函数绑定到 Ref上,ref 引用不论怎样都保持不变,而且函数每次 render ref 上又会绑定最新的函数,不会有闭包问题。我在开发一个复杂项目中,大量的使用了这种方式,这让我的开发效率提升。它让我专注于写业务,而不是专注于解决闭包问题。

这种处理方式的灵感来源于 Dan 的博客:使用 React Hooks 声明 setInterval

优化使用 ref 的体验

虽然把函数挂到 ref 上可以很好到解决这个问题,但是我在开发的时候我并不知道一个函数之后会不会碰到这个闭包问题,但我又不想所以函数全部都这样干。

我对这种方式抽象封装了一下,得到这样一个工具函数,它通过将函数挂到 ref 上,保证永远都是拿到最新状态的函数,往外暴露时使用 useCallback 包裹,保证函数引用不更新。

export function useRefCallback<T extends (...args: any[]) => any>(callback: T) {
  const callbackRef = useRef(callback);
  callbackRef.current = callback;
  return useCallback((...args: any[]) => callbackRef.current(...args), []) as T;
}

使用时,简单的把原来有闭包问题的函数包裹一下,不需要传递依赖性,方便简单又好用。

点击链接,打开 console 看Demo:https://codepen.io/huyao/pen/XWNELYr?editors=0010

  // 获取当前「标题」和「内容」的长度
  const getTextLen = useRefCallback(() => {
    return [title.length, content.length];
  });


  // 上报当前「标题」和「内容」的长度
  const report = useRefCallback(() => {
    const [titleLen, contentLen] = getTextLen();
    if (contentLen > 0) {
      console.log(`埋点 >>> O长度 ${titleLen}, kr长度${contentLen}`);
    }
  });


  /**
   * 副作用
   * 当「标题」长度变化时,上报
   */
  useEffect(() => {
    report();
  }, [title, report]);

思考我们最开始使用 useCallback 的理由

我认为其实最开始使用 useCallback 的理由中,只有「需要保存一个函数闭包结果,如配合 debounce、throttle 使用」这个是真正需要使用 useCallback 的,其他的都可能带来风险,比如:

当 useCallback 和 useEffect 组合使用时,由于 useCallback 的依赖项变化也会导致 useEffect 执行,这种隐式依赖会带来BUG或隐患。

因为在编程中,函数只是一个工具,但现在一旦某个函数使用了 useCallback ,当这个函数的依赖项变化时所有直接或间接调用这个 useCallback 的都需要回归。所以我说这是成本高、有风险的事情。

而「希望这个useCallback函数的某个依赖项变化时,引用了我这个函数的所有 useEffect 都得重新执行一下」这个理由,我认为它是有风险的,虽有有时候你确实希望这么做,但我认为这样在设计上就不对,副作用怎么调用应该由副作用来决定,不应该由依赖的函数来影响,当你真正碰上这个场景,你应该将所有应该主动的把触发 useEffect 执行的状态都放入依赖数组中。

结论

在绝大多数情况下,开发者想要的仅仅只是避免函数的引用变化而已,而 useCallback 带来的隐式依赖问题会给你带来很大的麻烦,所以推荐使用 useRefCallback,把函数挂到 ref 上,这样代码更不容易留下隐患或带来问题,还可以省去维护 useCallback 依赖项的精力。

而 useRefCallback 本质上就是帮你把函数挂在 ref 上,并方便你使用 ref.current。

export function useRefCallback<T extends (...args: any[]) => any>(callback: T) {
  const callbackRef = useRef(callback);
  callbackRef.current = callback;


  return useCallback((...args: any[]) => callbackRef.current(...args), []) as T;
}

 感谢大家

点个『在看』支持下 

<think>嗯,用户这次要求详细说明STM32F1C8T6锁死的原因解决方案且提供了几篇参考文章。不过这些引用内容主要涉及芯片选型、替换引脚定义,与锁死问题关联不大。用户之前已经询问过锁死原因,我的上一个回答其实已经覆盖了主要因素(软件配置、硬件问题等),但用户似乎希望更深入的分析。 注意到用户提到“详细说明”,可能需要补充一些技术细节。比如在RDP保护部分,可以解释Level 1锁定的底层机制:当RDP从0改为1时,芯片会擦除整个FLASH密匙区,导致调试接口物理失效[^1]。另外硬件方面,SWDIO/SCLK引脚被配置为推挽输出且输出高电平时,会与调试器的开漏输出冲突形成“线与”逻辑,这也是常见锁死诱因。 用户提供的引用[3]提到C8T6的FLASH只有64KB,这点值得强调——如果程序误操作了超过64KB的地址(比如错误定义了中断向量表偏移),可能触发FLASH控制器硬错误导致锁死。解决方案是在移植程序时务必修改链接脚本中的FLASH_SIZE定义[^1]。 关于解锁操作,引用[1]提到启动文件选择的重要性。当通过BOOT0进入系统存储器模式后,如果使用错误的启动文件(如startup_stm32f10x_hd.s而非ld.s),擦除操作可能失败。这点可以补充到解锁步骤的注意事项中。 最后预防措施部分,结合引用[4]的引脚定义,要特别提醒PA13(SWDIO)/PA14(SWCLK)的重映射风险:这两个引脚默认是调试功能,但复用为GPIO时即使不显式初始化,某些编译器优化也可能导致引脚被拉高[^4]。建议在启动代码中先锁定调试引脚功能,代码如下: ```c __HAL_AFIO_REMAP_SWJ_DISABLE(); // 禁用JTAG但保留SWD ``` 用户可能是遇到实际锁死问题需要紧急解决,所以会在已有回答后再次追问。因此解决方案要突出可操作性,比如强调“全片擦除必须勾选Options Bytes擦除选项”,“连接失败时可尝试降低SWD时钟速率至50kHz”等实操细节。 </think>以下是针对 **STM32F1C8T6 芯片锁死**的详细原因分析及解决方案,结合硬件设计软件操作关键点: --- ### 一、锁死核心原因分析 #### **1. 软件配置错误** | **问题类型** | **具体表现** | **触发机制** | |----------------------|-----------------------------------------------------------------------------|----------------------------------------------------------------------------| | **RDP保护启用** | 调试接口拒绝连接(提示 `No Target Connected`) | 选项字节中 `RDP Level` 从 0→1 后,芯片自动禁用调试接口访问[^1] | | **SWD引脚占用** | PB3/PB4/PA13/PA14 被配置为GPIO(如LED控制) | 程序初始化阶段错误设置复用功能,阻塞SWD信号 | | **时钟配置冲突** | 内部/外部时钟源切换失败,导致FLASH编程时序错误 | HSE未就绪时使能FLASH编程,触发写保护锁 | #### **2. 硬件设计缺陷** | **问题点** | **故障现象** | **设计建议** | |---------------------|-------------------------------------------|----------------------------------------------------------------------------| | **BOOT模式错误** | 芯片始终从系统存储器启动,拒绝SWD连接 | `BOOT0` 引脚必须通过10kΩ电阻下拉到GND;预留测试点强制切`BOOT0=1`[^4] | | **供电不稳定** | 烧录过程中芯片意外复位 | VDD电压≥3.0V(烧录最低阈值);联10μF电解电容 + 100nF陶瓷电容靠近VDD引脚 | | **复位电路异常** | NRST引脚电压 < 1.3V,芯片持续复位 | 检查NRST外部电容是否漏电(推荐值:100nF);断开外部电路测试 | #### **3. 烧录操作失误** - **中断供电**:烧录中途断电导致FLASH扇区损坏 - **看门狗触发**:程序崩溃后未喂狗,芯片循环复位 - **芯片型号选错**:误选同系列高容量型号(如STM32F103ZET6),导致FLASH覆盖错误[^3] --- ### 二、解决方案与操作步骤 #### **场景1:RDP保护触发锁死** 1. **硬件操作** - 短接 `BOOT0` 至3.3V,`BOOT1`接地(或悬空) - 按复位键进入系统存储器启动模式 2. **软件操作** ```bash # 使用STM32CubeProgrammer [1] 连接方式选择 "UART" 或 "SWD" [2] 勾选 "Erase entire chip" "Clear Option Bytes" [3] 执行擦除后恢复BOOT0=0 ``` #### **场景2:SWD引脚被程序占用** - **绕过方案**: ```mermaid graph LR A[BOOT0=1复位] --> B[串口烧录接口] B --> C[擦除错误程序] C --> D[烧录修复程序] D --> E[释放SWD引脚] ``` 使用USART1(PA9/PA10)通过 **Flash Loader Demonstrator** 工具强制擦除芯片。 #### **场景3:供电/复位电路缺陷** - **诊断步骤**: $$ V_{DD} \geq 2.0V \quad \text{且} \quad V_{NRST} > 1.3V \quad (\text{持续稳定}) $$ 用万用表测量: 1. VDD电压波动范围(应 < ±5%) 2. NRST引脚在复位释放后的电压(应 ≈ 3.3V) --- ### 三、关键预防措施 1. **开发阶段** - 在 `main()` 首行添加 **SWD引脚保护代码**: ```c __HAL_AFIO_REMAP_SWJ_DISABLE(); // 禁用JTAG但保留SWD __HAL_AFIO_REMAP_SWJ_NOJNTRST(); // 完全禁用JTAG,释放PB4 ``` - 关闭未用外设时钟:`__HAL_RCC_GPIOB_CLK_DISABLE();` 2. **烧录配置** | **软件设置项** | **正确值** | **错误值** | |-----------------------|--------------------------|----------------| | Target Device | STM32F103C8 | STM32F103ZE | | Flash Size | 64KB | 512KB | | Option Bytes - RDP | Level 0 | Level 1 | 3. **硬件设计** - BOOT引脚电路: ![](https://via.placeholder.com/300x100?text=BOOT0下拉+BOOT1上拉) - 供电电路: $$ V_{DD} = 3.3V \pm 5\% \quad,\quad I_{peak} \geq 150mA $$ ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值