概述
实现现实生活中的交通信号灯的切换,可能我们第一想法是使用定时器去切换,但是会存在一个问题,凡是使用到计时器的,都是不精准的,单次计时还好,但是会随着时间的推移,误差会被逐渐放大,会变得越来越不精确,因此下面我们使用问询模式来做这个功能。
计时器问题
前端计时器不精准的原因有如下:
- JavaScript 的单线程特性 :浏览器中 JavaScript 运行在单线程环境中,定时器(如
setTimeout
/setInterval
)的回调需要等待主线程空闲时才会执行。 - 事件循环的优先级:用户交互、页面渲染等任务的优先级高于定时器回调。如果主线程被阻塞(如执行长任务),定时器会被延迟,即使时间到了也无法立即执行。
具体可阅读下 John Resig(jQuery 作者)的这篇文章How JavaScript Timers Work
requestAnimationFrame
由于 setTimeout 和 setInterval 的不精准问题,促使了 requestAnimationFrame 的诞生。 requestAnimationFrame 是专门为实现高性能的帧动画而设计的一个API,目前已在多个浏览器得到了支持,你可以把它用在 DOM 上的效果切换或者 Canvas 画布动画中。 requestAnimationFrame 并不是定时器,但和 setTimeout 很相似,在没有 requestAnimationFrame 的浏览器一般都是用setTimeout模拟。 requestAnimationFrame 跟屏幕刷新同步(大多数是 60Hz )。确保动画帧率与屏幕刷新率一致。
实现
效果

目录结构
js
|------------------index.html
|------------------ index.js
程序
index.html
js
class TrafficLight {
constructor(lights) {
this.lights = lights;
this.currentIndex = 0; //当前信号灯的下标
this.switchTime = Date.now(); //切换到当前信号灯时间点
}
get current() {
return this.lights[this.currentIndex];
}
get disTime() {
return Date.now() - this.switchTime;
}
render(fn) {
requestAnimationFrame(this.render.bind(this, fn));
const current = this.getCurrentLight();
fn(current);
}
// 更新信号灯状态
update() {
while (true) {
if (this.disTime < this.current.latest) {
break; //当前信号灯的剩余时间>0,则不切换
} else {
this.currentIndex =
this.currentIndex + 1 > this.lights.length - 1
? 0
: this.currentIndex + 1;
this.switchTime = Date.now(); //更新切换时间
}
}
}
getCurrentLight() {
this.update();
return {
color: this.current.color,
remain: this.current.latest - this.disTime,
};
}
}
index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
#traffic-light {
width: 120px;
background-color: #333;
border-radius: 10px;
padding: 20px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
position: relative;
}
.light {
width: 100px;
height: 100px;
border-radius: 50%;
margin: 10px auto;
opacity: 0.3;
transition: opacity 0.3s;
position: relative;
}
#red {
background-color: red;
}
#yellow {
background-color: yellow;
}
#green {
background-color: green;
}
.active {
opacity: 1;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
}
.countdown {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30px;
font-weight: bold;
color: white;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
}
.controls {
margin-top: 30px;
text-align: center;
}
button {
padding: 8px 15px;
margin: 0 5px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
}
button:hover {
background-color: #45a049;
}
.timer-display {
text-align: center;
margin-top: 20px;
font-size: 24px;
font-weight: bold;
}
.red #red,
.yellow #yellow,
.green #green {
opacity: 1;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
}
.time {
color: #fff;
font-size: 25px;
}
</style>
</head>
<body>
<div id="traffic-light">
<div class="light red" id="red">
</div>
<div class="light yellow" id="yellow">
</div>
<div class="light green" id="green">
</div>
<div class="time"></div>
</div>
<script src="./index.js"></script>
<script>
const trafficLight = document.querySelector('#traffic-light');
const timeWrapper = document.querySelector('.time');
const light = new TrafficLight([
{
color: 'red',
latest: 7000
},
{
color: 'yellow',
latest: 3000
},
{
color: 'green',
latest: 10000
},
])
light.render((current) => {
console.log('current-', current)
trafficLight.className = `${current.color}`;
timeWrapper.textContent = (current.remain / 1000).toFixed(0) + "s"
})
</script>
</body>
</html>