设计一个交通灯 | 青训营笔记

174 阅读3分钟

这是我参与「第五届青训营」笔记创作活动的第 14 天

前言

如何设计一个交通灯,这是前端领域一个经典的问题,这样一个简单的问题却包含着很多的学问,设计的思路有非常多,本篇文章将结合青训营的JS相关的课程,为大家介绍几种不同的交通灯的设计方式。

设计方式

1. 普通方式

设计一个普通的交通灯并不困难,首先大家需要设计一个HTML的列表(ul)在上面设计五个子项,具体的代码如下

<style>
        #traffic {
            display: flex;
            flex-direction: column;
        }
        #traffic li {
            list-style: none;
            width: 60px;
            height: 60px;
            background-color: gray;
            margin: 5px;
            border-radius: 50%;
        }
        #traffic.s1 li:nth-child(1){
            background-color: red;
        }
        #traffic.s2 li:nth-child(2){
            background-color: #aa0;
        }
        #traffic.s3 li:nth-child(3){
            background-color: #0a0;
        }
        #traffic.s4 li:nth-child(4){
            background-color: #a0a;
        }
        #traffic.s5 li:nth-child(5){
            background-color: #0aa;
        }

    </style>
 <ul id="traffic" class="wait">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>

之后我们设置一个函数,使用setTimeOut来控制不同灯光变化的时间,具体的代码实现如下所示

const traffic = document.getElementById("traffic");
/**
 * 交通灯函数
 * 设置间隔时间,进行灯光变换
 */
function reset() {
  traffic.className = "s1";
  setTimeout(() => {
    traffic.className = "s2";
    setTimeout(() => {
      traffic.className = "s3";
      setTimeout(() => {
        traffic.className = "s4";
        setTimeout(() => {
          traffic.className = "s5";
          setTimeout(reset, 1000);
        }, 1000);
      }, 1000);
    }, 1000);
  }, 1000);
}

reset()

如上述代码所示,我们首先获取到ul标签,随后创建reset函数,第一次设置交通灯的为第一个灯,随后每隔1000ms都会切换一次灯光,当到最后一次时间间隔后我们重新传入reset函数,这样就可以实现交通灯的循环播放的功能

但是这样的写法出现了很多的问题,其中最明显的一个出现了回调地狱的问题,如果我们需要的灯光很多,这样代码的可读性就会非常低,并不是最优的选择

2. 数据抽象

我们使用数据抽象的方法,将我们交通灯的状态抽象成一个“状态列表”的方式,随后我们调用这个数据列表中的数据来操作交通灯的灯光变化,代码如下

//获取交通灯盒子的id
const traffic = document.getElementById('traffic')
//状态列表,定义了不同颜色的交通灯以及持续时间
const stateList = [    { state: 's1', last: 1000 },    { state: 's2', last: 1000 },    { state: 's3', last: 1000 },    { state: 's4', last: 1000 },    { state: 's5', last: 1000 },]

/**
 * 交通灯函数2(数据抽象)
 * @param {Element} traffic
 * @param {Array} stateList
 */
function start(traffic, stateList) {
    function applyState(stateIndex) {
        const { state, last } = stateList[stateIndex]
        traffic.className = state
        setTimeout(() => {
            applyState((stateIndex + 1) % stateList.length)
        }, last)
    }
    applyState(0)
}

start(traffic, stateList)

首先在函数中通过数据解构的方式解构出我们需要的state和last,然后利用setTimeOut来进行数据的绑定,每次在执行完一段时间后,设置状态变化(applyState((stateIndex + 1) % stateList.length)),通过这样来完成变化。

3. 过程抽象

与上一小节类似,这一次我们将过程抽象出来,具体的原理其实与上一个数据抽象类似,这里直接上代码把

//过程抽象
const traffic = document.getElementById('traffic')

/**
 * 等待函数(在等待一定时间后才会继续执行)
 * @param {Number} ms 
 * @returns 
 */
function wait(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms))
}

/**
 * 执行函数
 * @param  {...any} fnList 
 * @returns 
 */
function poll(...fnList) {
    let stateIndex = 0
    
    return async function (...args) {
        let fn = fnList[stateIndex++ % fnList.length]
        return await fn.apply(this, args)
    }
}
/**
 * 设置状态函数(通过此函数改变交通灯灯光)
 * @param {String} state 
 * @param {Number} ms 
 */
async function setState(state, ms) {
    traffic.className = state
    await wait(ms)
}

let trafficStatePoll = poll(setState.bind(null, 's1', 1000),
    setState.bind(null, 's2', 1000),
    setState.bind(null, 's3', 1000),
    setState.bind(null, 's4', 1000),
    setState.bind(null, 's5', 1000));

(async function () {
    //死循环一直执行
    while (43) {
        await trafficStatePoll()
    }
}())

总结

本篇文章是基于青训营课程的基础上进行简要的概括,其实许多的程序都可以套用这样的设计方式,例如过程抽象、数据抽象和函数式等等,这样的设计方式大大减少了初代方式的代码可读性低的问题,总而言之即减少了代码量,又增加了可读性,非常适合大家学习。