这是我参与「第五届青训营」笔记创作活动的第 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()
}
}())
总结
本篇文章是基于青训营课程的基础上进行简要的概括,其实许多的程序都可以套用这样的设计方式,例如过程抽象、数据抽象和函数式等等,这样的设计方式大大减少了初代方式的代码可读性低的问题,总而言之即减少了代码量,又增加了可读性,非常适合大家学习。