You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React单词游戏按下Enter时计时器短暂暂停的修复求助

问题

开发React单词游戏组件,内置60秒倒计时器。用户认读屏幕单词并输入,按Enter键后校验答案、更新得分、清空输入框继续游戏。但每次按Enter时计时器会短暂暂停,推测是重渲染导致,无法定位原因。

相关代码

TheBigScreen组件

function TheBigScreen() {

  const inputElement = useRef(null);

  // 正确/错误答案计数状态
  const [correctAnswers, setCorrectAnswers] = useState(0);
  const [incorrectAnswers, setIncorrectAnswers] = useState(0);

  // 单词数组与当前选中单词状态
  const wordsArray = ["apple", "banana", "cherry", "date", "elderberry"];
  const [selectedWord, setSelectedWord] = useState("");

  // 用户输入状态
  const [userInput, setUserInput] = useState("");

  // 单词显示/隐藏状态
  const [showWord, setShowWord] = useState(true);

  // ****************************** 函数定义 ****************************** //

  function chooseNewWord() {
    setSelectedWord(wordsArray[Math.floor(Math.random() * wordsArray.length)]);
    setShowWord(true);
  }

  // 输入框内容变化处理
  const handleInputChange = (e) => {
    setUserInput(e.target.value);
  };

  // 虚拟键盘输入处理
  const handleInputUpdateFromKeyboard = (character) => {
    const inputField = document.getElementById("input-field");
    if (inputField) {
      inputField.value += character;
      inputField.focus();
    }
  };

  // 答案校验逻辑
  const checkAnswer = () => {
    if (userInput === selectedWord) {
      setCorrectAnswers(correctAnswers + 1);
      setSelectedWord(() => {
        chooseNewWord();
      });
    } else {
      setIncorrectAnswers(incorrectAnswers + 1);
      handleWordClick();
    }
    setUserInput("");
  };

  // Enter键触发校验
  const handleKeyUp = (e) => {
    if (e.key === "Enter") {
      e.preventDefault();
      checkAnswer();
    }
  };

  // 点击隐藏单词的显示逻辑
  const handleWordClick = () => {
    setShowWord(true);
    if (inputElement.current) {
      inputElement.current.focus();
    }
    setTimeout(() => {
      setShowWord(false);
    }, 2000);
  };

  // 计时器结束回调
  const onTimerEnd = () => {
    console.log("Timer has ended.");
  };

  // *************************** 副作用处理 *************************** //

  useEffect(() => {
    chooseNewWord();
  }, []);

  // 单词自动隐藏逻辑
  useEffect(() => {
    let timer;
    if (selectedWord && showWord) {
      timer = setTimeout(() => {
        setShowWord(false);
      }, 3000);
    }
    return () => clearTimeout(timer);
  }, [selectedWord, showWord]);

  return (
    <div className="TheBigScreen-Container">
      <div className="GameHeader">
        <div className="Header-Back-Button">
          <BackButton
            title="Exit"
            onclick={handleExit}
            className="Back-Button"
          />
        </div>
        <div className="Header-Title">
          <h3 className="GameName">The Big Screen</h3>
        </div>
      </div>
      <div className="TheBigScreen-Content">
        <div className="TopLeft-Content">
          <Timer onTimerEnd={onTimerEnd} />
        </div>

        <div className="TopMiddle-Content">
          <div className="word-display">
            {showWord ? (
              <div className="word">{selectedWord}</div>
            ) : (
              <div className="word-blur" onClick={handleWordClick}>
                Word Hidden
              </div>
            )}
          </div>
          <div className="word-actions">
            <span className="icon" onClick={playSound}>
              <FaVolumeUp alt="Play Sound" className="PlaySoundIcon" />
            </span>
            <span className="icon" onClick={displayMeaning}>
              <FaBook alt="Display Meaning" className="DisplayMeaningIcon" />
            </span>
          </div>
        </div>

        <div className="TopRight-Content">
          <div className="answer-count">
            <FaCheckSquare alt="Tick" className="Tick" />
            <p>:</p>
            <p>{correctAnswers}</p>
          </div>
          <div className="answer-count">
            <FaCut alt="Cross" className="Cross" />
            <p>:</p>
            <p>{incorrectAnswers}</p>
          </div>
        </div>
      </div>

      <div className="Input-Content">
        <div className="Middle-Content">
          <div className="user-input">
            <input
              id="input-field"
              type="text"
              placeholder="Type your answer here"
              value={userInput}
              onChange={handleInputChange}
              maxLength="15"
              autoFocus
              autoComplete="off"
              spellCheck="false"
              onKeyUp={handleKeyUp}
              ref={inputElement}
            />
          </div>
        </div>
        <div className="Bottom-Content">
          <Keyboard onKeyboardButtonPressed={handleInputUpdateFromKeyboard} />
        </div>
      </div>
    </div>
  );
}

export default TheBigScreen;

Timer组件

const Timer = ({ startingTime = 60, onTimerEnd }) => {
  const [timeLeft, setTimeLeft] = useState(startingTime);

  useEffect(() => {
    if (timeLeft === 0) {
      onTimerEnd();
    } else {
      const timer = setInterval(() => {
        setTimeLeft((prevTime) => (prevTime > 0 ? prevTime - 1 : 0));
      }, 1000);
      return () => clearInterval(timer);
    }
  }, [timeLeft, onTimerEnd]);

  const radius = 40;
  const circumference = 2 * Math.PI * radius;
  const dashOffset =
    circumference - (circumference / startingTime) * (startingTime - timeLeft);

  return (
    <div className="timer-container">
      <svg className="timer-svg" width="120" height="120">
        <circle className="timer-circle" cx="60" cy="60" r={radius} />
        <text
          x="50%"
          y="50%"
          textAnchor="middle"
          dominantBaseline="middle"
          dy="0.25rem"
          className="timer-text"
        >
          {timeLeft}
        </text>
        <circle
          className="timer-path"
          cx="60" cy="60"
          r={radius}
          style={{
            strokeDasharray: circumference,
            strokeDashoffset: dashOffset,
          }}
        />
      </svg>
    </div>
  );
};

export default Timer;

问题分析

  1. Timer定时器频繁重启:Timer的useEffect依赖onTimerEnd,而该函数是TheBigScreen每次渲染时重新创建的,导致TheBigScreen每触发重渲染(比如按Enter更新得分),Timer就会清除旧定时器并创建新的,视觉上表现为计时器暂停。
  2. 答案校验逻辑冗余:正确答案分支中,setSelectedWord(() => { chooseNewWord(); })会触发两次setSelectedWord(一次在chooseNewWord内部,一次是函数式更新),引发额外重渲染。
  3. 虚拟键盘操作DOM导致状态不一致:handleInputUpdateFromKeyboard直接修改input的value而不更新userInput状态,可能引发React状态与DOM不一致,导致不必要的重渲染。

修复方案

1. 稳定Timer组件的定时器

修改Timer组件,用useRef保存最新的onTimerEnd,并仅在组件挂载时启动一次定时器:

const Timer = ({ startingTime = 60, onTimerEnd }) => {
  const [timeLeft, setTimeLeft] = useState(startingTime);
  const onTimerEndRef = useRef(onTimerEnd);

  // 实时更新ref保存最新的回调函数
  useEffect(() => {
    onTimerEndRef.current = onTimerEnd;
  }, [onTimerEnd]);

  useEffect(() => {
    const timer = setInterval(() => {
      setTimeLeft(prevTime => {
        if (prevTime <= 1) {
          clearInterval(timer);
          onTimerEndRef.current();
          return 0;
        }
        return prevTime - 1;
      });
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 仅挂载时启动一次定时器

  const radius = 40;
  const circumference = 2 * Math.PI * radius;
  const dashOffset = circumference - (circumference / startingTime) * (startingTime - timeLeft);

  return (
    <div className="timer-container">
      <svg className="timer-svg" width="120" height="120">
        <circle className="timer-circle" cx="60" cy="60" r={radius} />
        <text
          x="50%"
          y="50%"
          textAnchor="middle"
          dominantBaseline="middle"
          dy="0.25rem"
          className="timer-text"
        >
          {timeLeft}
        </text>
        <circle
          className="timer-path"
          cx="60" cy="60"
          r={radius}
          style={{
            strokeDasharray: circumference,
            strokeDashoffset: dashOffset,
          }}
        />
      </svg>
    </div>
  );
};

export default Timer;

2. 修复答案校验的冗余更新

去掉冗余的setSelectedWord调用,直接使用chooseNewWord,同时用函数式更新避免闭包问题:

const checkAnswer = () => {
  if (userInput === selectedWord) {
    setCorrectAnswers(prev => prev + 1);
    chooseNewWord();
  } else {
    setIncorrectAnswers(prev => prev + 1);
    handleWordClick();
  }
  setUserInput("");
};

3. 修复虚拟键盘的状态同步

通过更新userInput状态同步输入,而非直接操作DOM:

const handleInputUpdateFromKeyboard = (character) => {
  setUserInput(prev => prev + character);
  if (inputElement.current) {
    inputElement.current.focus();
  }
};

4. 稳定计时器结束回调(可选)

在TheBigScreen中用useCallback包裹onTimerEnd,避免每次渲染重新创建:

const onTimerEnd = useCallback(() => {
  console.log("Timer has ended.");
}, []);

验证

修改后,按Enter键时:

  • Timer组件的定时器不再频繁重启,消除暂停现象
  • 状态更新逻辑简洁,减少不必要的重渲染
  • 输入状态与DOM保持一致,消除潜在的重渲染触发点

内容的提问来源于stack exchange,提问作者BJAlcorn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 20:34:57