面试经典题之红绿灯:从异步控制到Promise深度考察

152 阅读6分钟

面试经典题之红绿灯:从异步控制到Promise深度考察

前言:为什么会有红绿灯这道面试题?

作为一名前端开发者,在面试中你是否遇到过这样的场景:面试官笑眯眯地问你:"能不能用代码实现一个红绿灯?" 你内心可能OS:"这是前端面试还是交通局招聘?" 🤔

实际上,这道题背后隐藏着面试官对候选人异步编程能力的深度考察。红绿灯问题本质上是一个状态循环与异步控制的问题,它考察的是:

  1. 对Promise的掌握程度
  2. 异步任务的顺序控制能力
  3. 代码的可读性与可维护性
  4. 如何处理可中断的异步操作

接下来,让我们一步步揭开这道题的神秘面纱!

从Fetch中断说起:AbortController与内存泄漏

在深入红绿灯问题之前,我们先看一个更常见的场景:如何中止fetch请求

为什么需要中止Fetch?

想象这样一个场景:用户在页面A发起了数据请求,但突然切换到了页面B。如果此时页面A的组件已经卸载,但请求还在进行中,就会导致:

  1. 内存泄漏:组件已卸载但回调函数仍可能执行
  2. 意外行为:请求返回后可能尝试更新已卸载的组件状态
  3. 资源浪费:不必要的网络请求和数据处理

AbortController:异步任务的"紧急停止按钮"

现代JavaScript提供了AbortController接口,它允许我们中止一个或多个Web请求。

import { useState, useEffect } from 'react';
import './App.css';

function App() {
  // 实例化一个控制器
  let controller = new AbortController();
  
  useEffect(() => {
    fetch('http://localhost:5173/api/banners', {
      // 接收信号
      signal: controller.signal,
    })
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(err => {
      if (err.name === 'AbortError') {
        console.log('请求已中止');
      } else {
        // 处理其他错误
      }
    });

  }, []);
  
  const stop = () => {
    // 中止请求
    controller.abort();
  }

  return (
    <>
      <button onClick={stop}>暂停</button>
    </>
  )
}

export default App

Signal:异步任务的中断信号

AbortSignalAbortController的信号对象,它可以传递给各种支持中止的API。当调用controller.abort()时,所有关联该信号的异步任务都会收到中止通知。

// 创建一个超时自动中止的信号
const signal = AbortSignal.timeout(1000);

// 检查信号是否已中止
if (signal.aborted) {
  // 处理中止情况
}

// 监听中止事件
signal.addEventListener('abort', () => {
  console.log('请求被中止了!');
});

Promise深度考题:红绿灯问题

现在回到我们的红绿灯问题。这道题本质上考察的是如何将异步操作组织成顺序执行的同步风格代码

解题思路分析

红绿灯的需求很简单:

  1. 红灯亮3秒
  2. 黄灯亮2秒
  3. 绿灯亮3秒
  4. 循环往复

但背后涉及的技术点很丰富:

  • 异步变同步的两种方式:async/await vs thenable
  • 循环控制
  • 状态管理

实现核心:Sleep函数

sleep函数的作用是创建一个可以等待指定时间的Promise,使得我们可以用同步的方式写异步代码。

  • 简洁版:等待指定时间后resolve,用于简单的延时。
  • 完整版:除了等待指定时间外,还可以接收一个AbortSignal信号,当信号被中止时,会清除定时器并拒绝Promise,从而能够中断等待。这样,我们就可以在异步流程中控制等待的可中断性,比如在处理红绿灯时,可以随时中止当前的等待。 JavaScript本身没有同步阻塞的sleep函数,但我们可以用Promise模拟:
// 简洁版
const sleep = ms => new Promise(r => setTimeout(r, ms));

// 完整版(支持中止信号)
const sleep = (ms, signal) => new Promise((resolve, reject) => {
  const timer = setTimeout(resolve, ms);
  
  // 监听中止事件
  signal.addEventListener('abort', () => {
    clearTimeout(timer);
    reject(new DOMException("Aborted", "AbortError"));
  }, { once: true });
});

const sleep = ms => new Promise(r => setTimeout(r, ms))解析

  • r 就是 resolve
  • setTimeout(r, ms) 表示:在 ms 毫秒后调用 resolve()
  • 所以这个 Promise 会在 ms 毫秒后自动变为 fulfilled 状态

等价于

const sleep = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(); // 没有传值,等价于 resolve(undefined)
  }, ms);
});

解决方案一:Async/Await方式

const sleep = ms => new Promise(r => setTimeout(r, ms));

async function trafficLight() {
  const seq = [
    { color: 'red', ms: 3000 },
    { color: 'yellow', ms: 2000 },
    { color: 'green', ms: 3000 },
  ];
  
  while (true) {
    for (const { color, ms } of seq) {
      console.log(color);
      await sleep(ms);
    }
  }
}

trafficLight();

优点

  • 代码清晰,接近同步代码的写法
  • 易于理解和维护

解决方案二:Thenable链式调用

function light(color, ms) {
  console.log(color);
  return new Promise(r => setTimeout(r, ms));
}

function loop() {
  light('red', 3000)
    .then(() => light('yellow', 2000))
    .then(() => light('green', 3000))
    .then(() => loop()); // 递归调用实现循环
}

loop();

优点

  • 纯Promise实现,不依赖async/await
  • 更好地理解Promise链的工作原理

高级进阶:可中止的红绿灯

在实际面试中,如果你能实现可控制启停的红绿灯,绝对是加分项!🎉

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>可控红绿灯</title>
  <style>
    .light {
      width: 100px;
      height: 100px;
      border-radius: 50%;
      margin: 10px;
      opacity: 0.3;
    }
    .active {
      opacity: 1;
    }
    #red { background-color: red; }
    #yellow { background-color: yellow; }
    #green { background-color: green; }
    #traffic-light {
      display: flex;
      flex-direction: column;
      align-items: center;
      width: 120px;
      background-color: #333;
      padding: 10px;
      border-radius: 10px;
    }
  </style>
</head>
<body>
  <div id="traffic-light">
    <div id="red" class="light"></div>
    <div id="yellow" class="light"></div>
    <div id="green" class="light"></div>
  </div>
  <button id="start">开始</button>
  <button id="stop">停止</button>
  <div id="status"></div>

  <script>
    const statusBox = document.getElementById('status');
    const redLight = document.getElementById('red');
    const yellowLight = document.getElementById('yellow');
    const greenLight = document.getElementById('green');
    
    // 支持中止的sleep函数
    const sleep = (ms, signal) => new Promise((resolve, reject) => {
      const timer = setTimeout(resolve, ms);
      signal.addEventListener('abort', () => {
        clearTimeout(timer);
        reject(new DOMException("Aborted", "AbortError"));
      }, { once: true });
    });

    const seq = [
      { color: 'red', ms: 3000, element: redLight },
      { color: 'yellow', ms: 2000, element: yellowLight },
      { color: 'green', ms: 3000, element: greenLight }
    ];

    let controller = null;

    async function trafficLight(signal) {
      // 重置所有灯
      seq.forEach(({ element }) => element.classList.remove('active'));
      
      while (!signal.aborted) {
        for (const { color, ms, element } of seq) {
          if (signal.aborted) return;
          
          // 更新UI
          seq.forEach(light => light.element.classList.remove('active'));
          element.classList.add('active');
          statusBox.textContent = `${color} - ${ms/1000}秒`;
          
          try {
            await sleep(ms, signal);
          } catch (err) {
            if (err.name === "AbortError") {
              statusBox.textContent = "已停止";
              return;
            }
            throw err;
          }
        }
      }
    }

    document.getElementById('start').addEventListener('click', () => {
      if (controller && !controller.signal.aborted) return;
      
      // 创建新的控制器
      controller = new AbortController();
      statusBox.textContent = "运行中...";
      trafficLight(controller.signal);
    });

    document.getElementById('stop').addEventListener('click', () => {
      if (controller) {
        controller.abort();
      }
    });
  </script>
</body>
</html>

这段代码实现了:

  1. 可视化红绿灯:通过CSS展示红绿灯状态变化
  2. 启停控制:通过AbortController实现中止功能
  3. 状态显示:实时显示当前灯状态和剩余时间
  4. 错误处理:正确处理中止异常和其他异常

面试官可能追问的Promise考题

  1. Promise基础

    • Promise的三种状态是什么?
    • Promise.resolve()和Promise.reject()的作用?
  2. 错误处理

    • catch()和then()的第二个参数有什么区别?
    • 如何实现全局的Promise错误捕获?
  3. 高级应用

    • 如何实现Promise.all?如果其中一个失败会怎样?
    • 如何实现Promise.race?有什么使用场景?
    • 如何实现Promise超时控制?
  4. 异步控制

    • 如何限制并发请求数量?
    • 如何实现请求重试机制?

总结与思考

红绿灯问题虽然看似简单,但涵盖了前端异步编程的多个重要概念:

  1. Promise核心概念:状态、链式调用、错误处理
  2. 异步控制:顺序执行、循环控制、可中止操作
  3. 实际应用:内存管理、用户体验优化

通过这道题,面试官可以全面考察候选人对JavaScript异步编程的理解深度和实际应用能力。

下次遇到这道题,你可以自信地回答:"我可以实现基础版本,还可以增加中止控制、可视化效果和状态提示..." 这样的回答一定会让面试官眼前一亮!✨