1

前两天看到一个别人写的面试经历一次羞愧难当的阿里前端笔试经历,本次换工作不会再寻求任何阿里工作机会,看了下题目自己试着写了下,短短续续差不多写了1天终于完后才能了。

实现一个红绿灯的需求

要求如下:

/** 1. 信号灯控制器
用 React 实现一个信号灯(交通灯)控制器,要求:

  1. 默认情况下,
    1.1. 红灯亮20秒,并且最后5秒闪烁
    1.2. 绿灯亮20秒,并且最后5秒闪烁
    1.3. 黄灯亮10秒
    1.4. 次序为 红-绿-黄-红-绿-黄
  2. 灯的个数、颜色、持续时间、闪烁时间、灯光次序都可配置,如:
    lights=[{color: '#fff', duration: 10000, twinkleDuration: 5000}, ... ]

*/

分析

js业务逻辑部分

先遍历一轮即先亮一组灯(默认红绿黄),再循环遍历灯

  1. 先要实现一个等待函数

首先有一个等待函数,即sleep函数
常用的sleep函数如下:

function sleep(time){
    return new Promise((resolve)=>{
        setTimeout(resolve,time)
    })
}

2.亮一颗灯逻辑
先亮N秒再闪M秒,其中M可能为0

async function Light(currentLight) {
    const current = currentLight
    console.log('current', current)
    const currentColor = current.color // 灯的颜色
    const currentTime = current.time // 常亮的时间
    const flashTime = current.flashingTime // 闪烁时间
    console.log(Date.now(), '开始亮', currentColor, '色的灯')
    await sleep(currentTime)
    if (flashTime > 0) {
    console.log(Date.now(), '开始闪', currentColor, '色的灯')
    await sleep(flashTime)
    }
    console.log(Date.now(), '关闭', currentColor, '色的灯')
}

3.亮一组灯的逻辑

async function roundLight(lights) {
    for (let item of lights) {
        console.log('遍历灯:', item.color)
        await Light(item)
    }
    return true // 添加返回值,标记一轮已结束
}

4.组与组间无限循环
首先想到的是setInterval(fn,time),其中time为组内所有的亮灯加闪灯的时间的和。

// 计算亮一轮灯需要的时间和
function getARoundTime(Lights) {
    let round = Lights
    let totalTime = 0
    for (let item of round) {
    totalTime += item.time + item.flashingTime
    }
    console.log('所有颜色的灯都亮完一轮需要时间为:', totalTime)
    return totalTime
}
// 首次写法是这样的
async function lightInterval(Lights) {
    // 获取亮一轮需要的时间
    let totalTime = getARoundTime(Lights)
    // 先亮一轮
    await roundLight(Lights)
    // 每隔一轮重新执行下一轮
    setInterval(async (Lights) => {
        await roundLight(Lights)
    }, totalTime)
}

执行后发现逻辑有问题,亮一轮后会等一轮时间后才再开始亮,后续正常亮
调整逻辑为先放个一轮的定时器,让第一轮开始时setInterval也开始执行
修改后变成了这样

async function lightInterval(Lights) {
    let totalTime = getARoundTime(Lights)
    setTimeout(() => {
        setInterval(async () => {
            await roundLight(Lights)
        }, totalTime)
    })
    await roundLight(Lights)
}

这样貌似可以,但是多看几轮会发现问题,会出现上一轮尚未完全结束就开始执行下一轮的情况
因为每一轮执行的时间并不完全精确等于要求的时间,会有毫秒级的误差
累积多了误差就明显了,所以不能使用setInterval

5.另一种组与组间无限循环的方法

async function roundInterval(Lights) {
    // 所有颜色的灯先亮一轮
    const roundResult = await roundLight(Lights)
    // 结束后调用自身,继续执行
    if (roundResult) {
        await roundInterval(Lights)
    }
}

js逻辑完成之后该写页面了

页面部分

  1. 如何在打console的地方执行页面内容的刷新?

通过setState让状态发生变化,自然页面就会刷新

// 先在页面上展示灯的状态及颜色
function ShowLight(props) {
    return (
        <span>
            当前展示的是{props.color},当前状态是{props.status}
        </span>
    )
}

使用state存储灯的颜色和状态,先显示一轮逻辑

    class LightTwo extends React.Component {
        constructor(props) {
          super(props)
          this.state = {
            currentColor: '',//保存灯的颜色
            currentStatus: '',//保存灯的状态
          }
        }
        async componentDidMount() {
          console.log('componentDidMount')
          //  先亮一颗灯试一下(取数组里的第一颗灯)
          const currentLight = this.props.lights[0]
          await this.LightState(currentLight)
        }
        async LightState(currentLight) {
          const currentColor = currentLight.color
          const currentTime = currentLight.time
          const flashTime = currentLight.flashingTime
          this.setState({ currentColor, currentStatus: 'on' })
          console.time('亮' + currentColor + '灯耗时')
          await sleep(currentTime)
          console.timeEnd('亮' + currentColor + '灯耗时')
          if (flashTime > 0) {
            this.setState({ currentColor, currentStatus: 'flash' })
            console.time('闪' + currentColor + '灯耗时')
            await sleep(flashTime)
            console.timeEnd('闪' + currentColor + '灯耗时')
          }
          // 一种颜色的灯亮过一次,清空
          this.setState({ currentColor: '', currentStatus: '' })
        }
        render() {
          return (
            <div>
              <ShowLight
                status={this.state.currentStatus}
                color={this.state.currentColor}
              />
            </div>
          )
        }
      }

然后显示多轮逻辑详见完整版

        // 遍历一组灯(红灯-绿灯-黄灯都亮过为一组)
        async roundLight() {
          const lights = this.props.lights
          if (lights.length !== 0) {
            for (var item of lights) {
              console.log('一组灯返回内判断---灯的数量是:', this.props.lights.length)
              if (this.props.lights.length === 0) {
                return false
              }
              console.log('遍历灯:', item.color)
              await this.LightState(item)
            }
            return true // 表示有灯 需要下一轮播放
          } else {
            return false // 表示没有灯了 不需要有下一轮了
          }
        }

2.灯的闪烁效果实现

通过调整透明度的动画无限循环来实现闪烁效果

@keyframes myAnimation {
    0% {
        opacity: 0;
        filter: alpha(opacity=0);
    }
    100% {
        opacity: 1;
        filter: alpha(opacity=100);
    }
}
.flash {
    -webkit-animation: myAnimation 0.6s infinite;
    animation: myAnimation 0.6s infinite;
}

3.react控制样式的展示

通过添加和移除className的方法来实现控制样式

render() {
    // 其他类名直接写死
    // 多个类名用空格拼接
    // 拼接当前颜色的类 
    let classNames = 'demo-1 ' + this.state.currentColor
    if (this.state.currentStatus) {
    //   拼接当前状态的类
    classNames += ' ' + this.state.currentStatus
    }
    return (
    <div>
        <ShowLight
           // 把类名传递给组件
            classNames={classNames}
            status={this.state.currentStatus}
            color={this.state.currentColor}
        />
    </div>
    )
}
// 显示组件
function ShowLight(props) {
    return (
        <>
        <span>
            {' '}
            当前展示的是{props.color},当前状态是{props.status}
        </span>
        <!-- 不能再这里写class="foo"(该类直接被废弃) 或者 className="foo"(若写在变量后面则变量不生效)  之类的写法 -->
        <span className={props.classNames}></span>
        </>
    )
}

停止红绿灯

// 方法一:清空函数 (这个逻辑实现有问题放弃该实现方案)
closeLight() {
    // 设置组间循环为空
    this.roundInterval = () => {}
    // 设置组内循环为空
    this.roundLight = () => {}
    // 设置控制单个灯的函数为空
    this.LightState = function () {}
    this.setState({
        currentColor: '',
        currentStatus: '',
    })
}
// 方法二:修改传入的灯数组为空数组
// 清空数组后还要在组与组循环间判断是否需要继续----通过标记位解决是否需要下一轮循环
// 还需要处理组(红绿黄)间暂停
// 还需要处理同一种颜色间暂停

封装红绿灯组件

class Traffic extends React.Component {
        constructor(props) {
          super(props)
          this.state = {
            lights: [],
          }
          this.closeLight = this.closeLight.bind(this)
          this.setLights = this.setLights.bind(this)
        }
        componentDidMount() {
          // console.log('componentDidMount--traffic---设置默认红绿灯')
          this.setLights()
        }
       // 可以设置为入参格式,便于更换其他颜色
        setLights() {
          const lights = [
            {
              color: 'red',
              time: 3000,
              flashingTime: 2000,
            },
            {
              color: 'green',
              time: 3000,
              flashingTime: 2000,
            },
            {
              color: 'yellow',
              time: 3000,
              flashingTime: 0,
            },
          ]
          this.setState({ lights: lights })
        }
        closeLight() {
          this.setState({ lights: [] })
        }
        render() {
          return (
            <div>
              <LightTwo lights={this.state.lights} />
              <button onClick={this.setLights}>设置默认红绿灯组</button>
              <button onClick={this.closeLight}>关闭红绿灯组</button>
            </div>
          )
        }
      }

落叶飘飘
76 声望2 粉丝