做个交通信号灯特效

概述

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

计时器问题

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

  • 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>
相关推荐
tianzhiyi1989sq19 分钟前
Vue3 Composition API
前端·javascript·vue.js
今禾26 分钟前
Zustand状态管理(上):现代React应用的轻量级状态解决方案
前端·react.js·前端框架
用户25191624271127 分钟前
Canvas之图形变换
前端·javascript·canvas
今禾35 分钟前
Zustand状态管理(下):从基础到高级应用
前端·react.js·前端框架
gnip41 分钟前
js模拟重载
前端·javascript
Naturean44 分钟前
Web前端开发基础知识之查漏补缺
前端
curdcv_po1 小时前
🔥 3D开发,自定义几何体 和 添加纹理
前端
单身汪v1 小时前
告别混乱:前端时间与时区实用指南
前端·javascript
鹏程十八少1 小时前
2. Android 深度剖析LeakCanary:从原理到实践的全方位指南
前端
我是ed1 小时前
# cocos2 场景跳转传参
前端