做个交通信号灯特效

概述

实现现实生活中的交通信号灯的切换,可能我们第一想法是使用定时器去切换,但是会存在一个问题,凡是使用到计时器的,都是不精准的,单次计时还好,但是会随着时间的推移,误差会被逐渐放大,会变得越来越不精确,因此下面我们使用问询模式来做这个功能。

计时器问题

前端计时器不精准的原因有如下:

  • 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>
相关推荐
不坑老师12 分钟前
不坑盒子的插入网页功能是完全免费的!
前端·html
Wang's Blog15 分钟前
前端FAQ: 描述⼀下你最近使⽤过的前端框架,并解释为何选择它们?
前端·vue.js·faq
wgego16 分钟前
做题笔记BUU (XSS-Lab)(1-14)
前端·笔记·xss
dllxhcjla31 分钟前
css第二天
java·前端·css
远航_34 分钟前
10 个被严重低估的 JS 特性,直接少写 500 行代码
前端·javascript
起这个名字34 分钟前
感觉这篇 DeepSeek 给出的微前端实践很牛!轻喷!
前端
小高00735 分钟前
当前端面临百万级 API 请求:从"修 CSS 的"到架构师的进化之路
前端·javascript·面试
P7Dreamer36 分钟前
package.json:你以为只是配置文件,其实是项目的命门!
前端·node.js
BBB努力学习程序设计37 分钟前
让数据阅读更轻松:隔行换色表格的妙用
前端·html
西甲甲38 分钟前
Process Explorer、windows 任务管理器以及系统 API 之间的关系
前端