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获取新时长。 - 问题随机出现是因为点击按钮的时机不确定:如果点击后刚好在定时器执行前完成页面跳转/重新加载,就不会触发;如果点击后定时器先执行了写操作,就会出现复用旧数据的情况。
修复方案
- 全局管理定时器实例,切换时清除旧定时器
在全局作用域定义变量存储定时器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); } - 点击切换按钮时强制终止定时器
确保切换测试时彻底停止上一场的计时任务:nextButton.click(function () { if (testTimer) { clearInterval(testTimer); testTimer = null; } indexSubtest++; sessionStorage.setItem("indexSubtest", indexSubtest); sessionStorage.removeItem("timeSubtest"); }); - 优化计时逻辑,减少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
相关产品推荐
相关产品推荐

