深入拆解一道真实的字节面试题来学习 AJAX

三葉Leaves Author

这是一道真实的字节跳动的面试题,通过这个可以加深对 AJAX 的整体理解。

题目

用程序模拟:

红灯每次亮 3 秒,绿灯每次亮 2 秒,黄灯每次亮 1 秒且交替循环,要求用原生 Promise 实现,最好还能加入更多控制 (提示:可以写一个 delay 函数)。

解法1: 同步函数

字节的面试一般会逐渐深入,我这里也模拟这个渐进式过程,先从最简单的实现开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function sleep(ms) {
const start = Date.now();
// 这个循环会独占 CPU,直到时间结束
while (Date.now() - start < ms) {
// do nothing
}
}

function runBadTrafficLight() {
while(true) {
console.log('红灯亮了');
sleep(3000); // 浏览器会卡死 3 秒
console.log('绿灯亮了');
sleep(2000); // 浏览器再卡死 2 秒
console.log('黄灯亮了');
sleep(1000); // 浏览器再卡死 1 秒
}
}

runBadTrafficLight(); // 千万不要在浏览器控制台运行这个!它会让页面完全卡死!

这个实现完全是同步函数的方式,虽然糟糕,但是确实完成了任务。

其中的 sleep() 函数的思想其实就是我们接下来要实现的 delay() 方法,只不过前者会完全卡死程序,后者可以优雅的完成这个”等待“需求。

解法2: promise 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 红灯亮 3 秒,绿灯亮 2 秒,黄灯亮 1 秒交替循环。用原生 Promise 实现 delay 函数,最好还能加入更多控制。

const lightUp = (type, time) => {

return new Promise((resolve) => {
console.log(`开始亮 ${type} 灯` + new Date().getSeconds());
setTimeout(() => {
console.log("灯熄灭了" + new Date().getSeconds());
resolve();
}, time);
});
};

const runLights = () => {
lightUp("red", 3000)
.then(() => {
return lightUp("green", 2000);
})
.then(() => {
return lightUp("yellow", 1000);
})
.then(() => {
console.log("Another Loop");
runLights();
})
.catch(() => {
console.log('loop stop');
});
};

runLights();

这段代码有两个核心点:

  1. 使用 链式调用 完成”等待“需求。

每一个 .then 等会返回一个 promise ,下一个 .then 在上一个 .then 返回的 promise 敲定前(敲定指的是返回的 promise 得到 resolve 或者 reject ,而变成 fulfilled 或者 rejected 的状态)不会执行,而是一直等待上一个 .then() 返回的 promise 敲定。

通过这点,完美实现里上一个灯熄灭前下一个灯会一直等待的核心需求和难点。

  1. 使用递归的方式完成”循环进行永不停止“的需求

可见第三个 .then 里的回调。

解法3:使用 async 语法糖优化刚才的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 红灯亮 3 秒,绿灯亮 2 秒,黄灯亮 1 秒交替循环。用原生 Promise 实现 delay 函数,最好还能加入更多控制。

const lightUp = (type, time) => {
return new Promise((resolve) => {
console.log(`开始亮 ${type} 灯` + new Date().getSeconds());
setTimeout(() => {
console.log("灯熄灭了" + new Date().getSeconds());
resolve();
}, time);
});
};

const runLight = async () => {
await lightUp("red", 3000);
await lightUp("green", 2000);
await lightUp("yellow", 1000);
console.log('Another loop');
await runLight()
};

runLight();

lightUp 函数没有发生变化,下面的部分只是用语法糖重写了一下,也没有加入停止的方法,没什么好说的。

解法3:升级的 async 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class TrafficLight {
constructor() {
this.isRunning = false;
}

// 创建延时 Promise
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// 点亮指定颜色的灯
async lightUp(color, duration) {
console.log(`${color}灯在时刻 ${new Date().getSeconds()} 亮了`);
await this.delay(duration);
console.log(`${color}灯在时刻 ${new Date().getSeconds()} 灭了`);
}

// 交替闪烁循环
async startBlinking() {
this.isRunning = true;

while (this.isRunning) {
try {
// 红灯亮 3 秒
await this.lightUp('红', 3000);
if (!this.isRunning) break;

// 绿灯亮 2 秒
await this.lightUp('绿', 2000);
if (!this.isRunning) break;

// 黄灯亮 1 秒
await this.lightUp('黄', 1000);
if (!this.isRunning) break;

} catch (error) {
console.error('信号灯运行出错:', error);
break;
}
}

console.log('信号灯已停止');
}

// 停止闪烁
stop() {
this.isRunning = false;
}
}

// 使用示例
const trafficLight = new TrafficLight();

// 启动信号灯
trafficLight.startBlinking();

// 10 秒后停止
setTimeout(() => {
trafficLight.stop();
}, 10000);

console.log('信号灯程序已启动');

主要有三点升级:

  1. 创建了 delay 函数模拟灯的常亮,业务逻辑更分离。
  2. 使用了面向对象的编程设计,更内聚、更易于扩展。
  3. 每一段信号灯之间有一次停止的机会,不过信号灯中间不能停。

解法4:使用 Promise.race 实现可中断的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class InterruptibleTrafficLight {
constructor() {
this.shouldStop = false;
}

delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

createInterruptibleDelay(ms) {
const delayPromise = this.delay(ms);
const stopPromise = new Promise(resolve => {
const checkStop = () => {
if (this.shouldStop) {
resolve('stopped');
} else {
setTimeout(checkStop, 100);
}
};
checkStop();
});

return Promise.race([delayPromise, stopPromise]);
}

async lightUp(color, duration) {
console.log(`${color}灯在时刻 ${new Date().getSeconds()} 亮了`);
const result = await this.createInterruptibleDelay(duration);

if (result === 'stopped') {
console.log(`${color}灯被中断`);
return false;
}

console.log(`${color}灯在时刻 ${new Date().getSeconds()} 灭了`);
return true;
}

async start() {
const sequence = [
{ color: '红', duration: 3000 },
{ color: '绿', duration: 2000 },
{ color: '黄', duration: 1000 }
];

while (!this.shouldStop) {
for (const light of sequence) {
const continued = await this.lightUp(light.color, light.duration);
if (!continued) return;
}
}
}

stop() {
this.shouldStop = true;
}
}

// 使用示例
// const light = new InterruptibleTrafficLight();
// light.start();
// setTimeout(() => light.stop(), 8000); // 8秒后停止

这个版本实现了一个可中断的 delay 函数,使得即便灯在亮起的过程中也能被突然打断。

核心原理是这个函数:

1
2
3
4
5
6
7
8
9
10
const stopPromise = new Promise(resolve => {
const checkStop = () => {
if (this.shouldStop) {
resolve('stopped');
} else {
setTimeout(checkStop, 100);
}
};
checkStop();
});

它每隔 100ms 递归的调用自己来检查信号量 shouldStop 的状态。

Promise.race() 接受一个数组或者可迭代对象,返回一个 Promise。这个返回的 promise 会随着第一个 promise 的敲定而敲定。

这个版本的代码利用了这个特点比较延迟函数和停止函数谁先敲定,由此一来在研究过程中如果停止函数突然 resolve ,那么返回的 promise 也被敲定为停止函数了。

除此之外,灯的顺序和亮起时长也抽取成一个对象数组,这使得程序逻辑更清晰,更符合现代前端开发的惯例。

  • 标题: 深入拆解一道真实的字节面试题来学习 AJAX
  • 作者: 三葉Leaves
  • 创建于 : 2025-07-25 00:00:00
  • 更新于 : 2025-08-13 16:31:27
  • 链接: https://blog.oksanye.com/3632a75d0b18/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论