Fullstack / Frontend(多帖) · Technical phone screen 第一轮

Salesforce 原生 JS 红绿灯:4/1/1 秒切换与计时器清理

Salesforce Fullstack 电面要求不用 React、用 HTML/CSS/JS 写红绿灯,红 4 秒黄 1 秒绿 1 秒;本文给完整独立页面和虚拟时钟验证。

题目重点:Salesforce Fullstack 电面要求不用 React、用 HTML/CSS/JS 写红绿灯,红 4 秒黄 1 秒绿 1 秒;本文给完整独立页面和虚拟时钟验证。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练版选择红→绿→黄→红循环,持续时间分别 4000/1000/1000 毫秒;颜色时长来自题目,顺序是本文明确补充的约定。
  • 页面初始立即显示红灯,每次只有一盏灯亮;不加入暂停、倒计时或行人按钮,因为题目没有要求。
  • 显示还用文字状态和 aria-label,不只靠颜色辨识;页面离开时清理 pending timer。setTimeout 实际触发可能延后,不保证墙钟上的毫秒级精确。

解题路线

  1. 把每个阶段的 id、文字和时长放在同一 phases 配置数组,index 指向当前阶段。render 根据 index 同步更新三盏灯的 active 类、ARIA 标签和可读文字,保持 UI 单一状态源。
  2. 初始 render 红灯后,按当前阶段时长安排一次 setTimeout;计时到期时 advance 把 index 循环加一,先渲染再按新阶段时长安排下一次。递归单个 timer 避免多组 interval 分别控制灯、产生重叠亮灯。
  3. 生命周期结束时 clearTimeout 最新 timer。用虚拟时钟检查 t=0/3999/4000/5000/6000 等边界,确认每次切换只发生一次;若用户要后台时间精确同步,应以绝对 deadline 计算当前状态,而不是假设定时器准点。

手动走一遍

t=0 红灯亮;t=3999ms 仍红;t=4000ms 绿灯亮;t=5000ms 黄灯亮;t=6000ms 回红,下一轮继续。状态文字同步更新为‘当前:红灯/绿灯/黄灯’。离开页面时取消下一次回调。

HTML 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>红绿灯计时练习</title>
  <style>
    body { font: 1rem/1.5 system-ui, sans-serif; padding: 2rem; }
    .signal { display: flex; gap: 1rem; padding: 1rem; width: max-content;
      border-radius: 1rem; background: #252525; }
    .lamp { width: 3rem; height: 3rem; border-radius: 50%; opacity: .25; }
    .lamp.active { opacity: 1; outline: 3px solid white; outline-offset: 3px; }
    #red { background: #e33b3b; }
    #yellow { background: #efc337; }
    #green { background: #32be62; }
  </style>
</head>
<body>
  <h1>红绿灯</h1>
  <div class="signal" role="group" aria-label="当前交通信号">
    <span id="red" class="lamp" aria-label="红灯关闭"></span>
    <span id="yellow" class="lamp" aria-label="黄灯关闭"></span>
    <span id="green" class="lamp" aria-label="绿灯关闭"></span>
  </div>
  <p id="status" role="status" aria-live="polite"></p>
  <script>
    const phases = [
      { id: "red", label: "红灯", duration: 4000 },
      { id: "green", label: "绿灯", duration: 1000 },
      { id: "yellow", label: "黄灯", duration: 1000 },
    ];
    let index = 0;
    let timerId = null;

    function render() {
      for (const phase of phases) {
        const lamp = document.getElementById(phase.id);
        const active = phase.id === phases[index].id;
        lamp.classList.toggle("active", active);
        lamp.setAttribute("aria-label", `${phase.label}${active ? "亮起" : "关闭"}`);
      }
      document.getElementById("status").textContent = `当前:${phases[index].label}`;
    }

    function advance() {
      index = (index + 1) % phases.length;
      render();
      timerId = setTimeout(advance, phases[index].duration);
    }

    render();
    timerId = setTimeout(advance, phases[index].duration);
    window.addEventListener("pagehide", () => clearTimeout(timerId), { once: true });
  </script>
</body>
</html>

复杂度

每次切换只扫描三个固定灯并设一个 timer,时间和额外状态均 O(1);若扩展到 L 个可配置灯,render 为 O(L)。setTimeout 的延时是最短等待而非精确执行时刻,浏览器后台限速与主线程阻塞可能造成周期漂移;这是交互计时的工程边界。

易错点与追问

容易失分的边界

  • 为三个灯分别设置 interval,计时器相位逐渐错开,出现同时亮或空档。
  • 切换后继续使用上一阶段的 duration,红灯 4 秒被错误应用到绿灯。
  • 只改变 CSS 颜色、不更新文字和可访问名称,屏幕阅读器无法获得状态。
  • 把红→绿→黄顺序当作题目明示条件;题目只给各灯时长。
  • 页面离开后仍保留计时器,或在反复初始化组件时叠加多个循环。

继续深挖

  • 若要暂停/继续,记录剩余时长和绝对截止时间,如何避免多次点击生成重叠 timer?
  • 若后台标签页计时被限速,回来后应继续原阶段还是根据墙钟跳过若干周期?先确定产品语义。
  • 若交通灯要支持行人请求,如何把相位和过渡规则建为显式状态机,而非只在数组里轮转?
✓已复制微信号!