做个交通信号灯特效

概述

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

计时器问题

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

  • 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>
相关推荐
李剑一几秒前
别再瞎写 Cesium 可视化!热力图 + 四色图源码全公开,项目直接复用!
前端·vue.js·cesium
SuperEugene2 分钟前
Vue3 + Vue Router + Pinia 路由守卫规范:beforeEach 应做 / 不应做,避死循环、防重复请求|状态管理与路由规范篇
开发语言·前端·javascript·vue.js·前端框架
Greg_Zhong4 分钟前
Css知识之伪类和伪元素
前端·css
Mintopia5 分钟前
GPT-5.3-Codex 底层逻辑是什么,为什么编码强?
前端·人工智能·ai编程
Mintopia6 分钟前
Opus 模型凭什么收费贵,与其他模型对比理由是什么?
前端·人工智能
东东__net7 分钟前
js逆向与谷歌加密库
开发语言·前端·javascript
程序员小郭8316 分钟前
Spring Ai 05 ChatClient Advisor 实战(日志、提示词增强、内容安全)
java·开发语言·前端
m0_5027249516 分钟前
腾讯地图tlbs-multi-marker动态更新marker图标
前端·javascript·vue.js·地图
IT_陈寒20 分钟前
SpringBoot 项目启动慢?这5个优化技巧让你的应用快50%
前端·人工智能·后端
GISer_Jing30 分钟前
React核心语法:组件化与声明式编程
前端·react.js·前端框架