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

JavaScript考试系统计时的竞态条件问题排查

问题分析与解决方案

问题描述

我开发了一个考试网站,将测试时长存储在sessionStorage中,避免页面刷新时重复计时,仅在开始下一场测试时重置计时。但部分场景下,第二场测试仍沿用第一场的计时数据,经排查怀疑是竞态条件。推测是:点击nextButton按钮时虽成功删除了sessionStorage中的timeSubtest,但countdownTimer每秒都会设置该存储项,覆盖了已删除的数据。

代码片段

$(document).ready(function () {
  displaytest(testId, indexSubtest);
});

function displaytest(testId, indexSubtest) {
  $.ajax({
    url: url,
    method: "GET",
    dataType: "json",
    success: function (data) {
      countdownTImer();

      var nextButton = $("#nextButton");
      var finishButton = $("#finishButton");

      nextButton.click(function () {
        indexSubtest++;
        sessionStorage.setItem("indexSubtest", indexSubtest);
        sessionStorage.removeItem("timeSubtest");
      });
      finishButton.click(function () {
        storeAnswer(stored);
      });
    },
    error: function (error) {
      console.error("error:", error);
    },
  });
}

function countdownTImer() {
  const isActiveSubtest = sessionStorage.getItem("indexSubtest");
  let time = sessionStorage.getItem("timeSubtest");

  if (!time) {
    time = dataUjian.subtest[isActiveSubtest - 1].subtesDurations[0];
    sessionStorage.setItem("timeSubtest", time);
  }

  var x = setInterval(function () {
    time -= 1;
    sessionStorage.setItem("timeSubtest", time);
    const seconds = time % 60;
    const secondsInMinutes = (time - seconds) / 60;
    const minutes = secondsInMinutes % 60;

    document.getElementById("timeTest").innerHTML =
      minutes + "m:" + seconds + "s";

    if (time < 0 ) {
      sessionStorage.removeItem("timeSubtest");
      forceFinish(1, isActiveSubtest);
      clearInterval(x);
    }
  }, 1000);
}

结论:确实是竞态条件导致的

问题出在点击nextButton时的操作和countdownTimer的定时器任务存在竞态:

  • 点击按钮后,代码先删除sessionStorage里的timeSubtest,但此时上一场测试的定时器还在运行,下一秒定时器会再次把当前的time值写回sessionStorage。
  • 之后加载第二场测试时,countdownTimer读取到的就是被定时器覆盖的旧计时数据,而非重新从dataUjian获取新时长。
  • 问题随机出现是因为点击按钮的时机不确定:如果点击后刚好在定时器执行前完成页面跳转/重新加载,就不会触发;如果点击后定时器先执行了写操作,就会出现复用旧数据的情况。

修复方案

  1. 全局管理定时器实例,切换时清除旧定时器
    在全局作用域定义变量存储定时器ID,确保每次启动新计时前终止旧任务:
    let testTimer = null; // 全局存储定时器ID
    
    function countdownTImer() {
      // 先清除之前的定时器,防止多个定时器同时运行
      if (testTimer) {
        clearInterval(testTimer);
      }
      const isActiveSubtest = sessionStorage.getItem("indexSubtest");
      let time = sessionStorage.getItem("timeSubtest");
    
      if (!time) {
        time = dataUjian.subtest[isActiveSubtest - 1].subtesDurations[0];
        sessionStorage.setItem("timeSubtest", time);
      }
    
      testTimer = setInterval(function () {
        time -= 1;
        sessionStorage.setItem("timeSubtest", time);
        const seconds = time % 60;
        const secondsInMinutes = (time - seconds) / 60;
        const minutes = secondsInMinutes % 60;
    
        document.getElementById("timeTest").innerHTML =
          minutes + "m:" + seconds + "s";
    
        if (time < 0 ) {
          sessionStorage.removeItem("timeSubtest");
          forceFinish(1, isActiveSubtest);
          clearInterval(testTimer);
          testTimer = null;
        }
      }, 1000);
    }
    
  2. 点击切换按钮时强制终止定时器
    确保切换测试时彻底停止上一场的计时任务:
    nextButton.click(function () {
      if (testTimer) {
        clearInterval(testTimer);
        testTimer = null;
      }
      indexSubtest++;
      sessionStorage.setItem("indexSubtest", indexSubtest);
      sessionStorage.removeItem("timeSubtest");
    });
    
  3. 优化计时逻辑,减少sessionStorage依赖
    将当前计时存在内存变量中,仅在页面刷新时从sessionStorage恢复,从根源降低竞态概率:
    let currentTime = null;
    let testTimer = null;
    
    function countdownTImer() {
      if (testTimer) clearInterval(testTimer);
      const isActiveSubtest = sessionStorage.getItem("indexSubtest");
      // 优先从内存读取,页面刷新时才从sessionStorage恢复
      if (!currentTime) {
        currentTime = sessionStorage.getItem("timeSubtest") || dataUjian.subtest[isActiveSubtest - 1].subtesDurations[0];
        sessionStorage.setItem("timeSubtest", currentTime);
      }
    
      testTimer = setInterval(function () {
        currentTime -= 1;
        sessionStorage.setItem("timeSubtest", currentTime);
        const seconds = currentTime % 60;
        const secondsInMinutes = (currentTime - seconds) / 60;
        const minutes = secondsInMinutes % 60;
    
        document.getElementById("timeTest").innerHTML =
          minutes + "m:" + seconds + "s";
    
        if (currentTime < 0 ) {
          sessionStorage.removeItem("timeSubtest");
          forceFinish(1, isActiveSubtest);
          clearInterval(testTimer);
          testTimer = null;
          currentTime = null;
        }
      }, 1000);
    }
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 21:36:02