React Hooks中使用useState导致setTimeout多次执行的原因及解决方法
为什么会出现多次执行的情况?
咱们先拆解下核心原因:React函数组件(包括自定义Hooks)每次状态更新都会重新执行整个组件/函数体。
在你的usePassword Hook里,let timer = null是函数内部的局部变量——每次组件因为setPasswordValue触发重新渲染时,这个timer都会被重新初始化为null,同时trigger函数也会被重新创建。
这就导致了一个关键问题:当你输入内容调用onPasswordChanged时,每次调用的trigger都是当前渲染周期的新函数,它访问的timer也是当前的新变量。而之前渲染周期里创建的定时器,对应的旧timer变量已经被“丢”在旧的闭包里了,新的clearTimeout根本清不掉它。结果就是每输入一次,就会留下一个没被清理的定时器,到时间后自然会依次执行console.log("end")。
而类组件里的timer是挂载在实例上的(比如this.timer),不会随着渲染重新创建,所以每次都能正确清除旧定时器;不用useState的话,组件不会触发重新渲染,整个usePassword只会执行一次,timer也只有一个,自然不会有重复执行的问题。
怎么解决这个问题?
最直接的方案是用useRef来保存定时器的引用——useRef的.current属性在组件整个生命周期里是同一个引用,不会随着渲染重置。
修改你的usePassword Hook如下:
import React, { useState, useRef } from "react"; const usePassword = () => { const [passwordValue, setPasswordValue] = useState({ password: "", passwordHidden: "", }); // 用useRef保存timer,替代普通变量 const timerRef = useRef(null); const trigger = () => { // 清除之前的定时器,访问current属性 clearTimeout(timerRef.current); // 把新定时器赋值给current timerRef.current = setTimeout(() => console.log("end"), 1000); }; const onPasswordChanged = (name, value) => { setPasswordValue((prev) => ({ ...prev, // 这里注意!之前用passwordValue存在闭包风险,改用prev参数更安全 passwordHidden: value })); trigger(); }; return { passwordValue, onPasswordChanged }; };
另外补充一个细节:你之前在setPasswordValue里用了...passwordValue,这存在闭包风险——passwordValue是当前渲染周期的状态,而setPasswordValue的回调参数prev才是最新的前状态,改成...prev能避免状态不一致的问题。
这样修改后,每次调用trigger时,都会清除上一次的定时器,不管组件渲染多少次,timerRef.current始终指向最新的定时器,就不会出现多次执行console.log的情况了。
内容的提问来源于stack exchange,提问作者Earn Asdsa

