🚦 掘金前端场景题:红黄绿灯的无限循环之旅
嘿,各位前端的"程序猿"们!今天咱们来聊一个前端面试中经常遇到的经典场景题------如何让红黄绿灯无限循环地亮起来?别看这只是个小小的红绿灯,它背后可是藏着前端异步编程的"大秘密"呢!
💡 问题重现:交通灯的"灵魂拷问"
想象一下,你站在一个十字路口,面前的红绿灯正在有节奏地闪烁:
- 红灯:亮3秒
- 绿灯:亮1秒
- 黄灯:亮2秒
现在问题来了:如何用代码实现这种红黄绿灯交替重复亮起的逻辑? 是不是听起来很简单?但要实现"无限循环",可就没那么容易了。这道题的精髓就在于,它不是让你亮一次就完事儿,而是要让它们像永动机一样,永远地亮下去!
为了模拟亮灯效果,我们先准备三个简单的函数:
javascript
function red() {
console.log("红灯亮了!");
}
function green() {
console.log("绿灯亮了!");
}
function yellow() {
console.log("黄灯亮了!");
}

🔄 Callback 实现:从"回调地狱"到"递归天堂"
第一次尝试:单次亮灯的"小把戏"
刚开始,你可能会想到用 setTimeout
来控制时间,然后用回调函数来衔接不同的灯光。我们先封装一个 task
函数,它负责在指定时间后亮起对应的灯,并执行下一个动作:
javascript
const task = (timer, light, callback) => {
setTimeout(() => {
if (light === 'red') {
red();
} else if (light === 'green') {
green();
} else if (light === 'yellow') {
yellow();
}
callback(); // 执行下一个回调
}, timer);
};
// 这样写,灯只会亮一次,然后就"熄火"了
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', Function.prototype); // Function.prototype 是一个空函数,表示没有后续操作
});
});
运行这段代码,你会发现红黄绿灯确实亮了一遍,但也就仅此而已了。它并没有实现我们想要的"无限循环"。就像你玩了一次过山车,然后就下车了,不能一直坐下去。
递归大法:让灯光"永不熄灭"
那么,如何让这三个小灯泡"永不熄灭"呢?答案就是------递归 !我们需要在黄灯亮起之后,再次触发整个亮灯的流程。我们可以定义一个 step
函数,它包含了红黄绿灯亮起的一个完整周期,然后在黄灯亮起的回调中再次调用 step
函数,形成一个完美的闭环:
javascript
const step = () => {
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', step); // 关键在这里!黄灯亮完后,再次调用step,实现循环
});
});
};
step(); // 启动我们的红绿灯循环!
是不是感觉有点意思了? 通过巧妙地在回调函数中调用自身,我们成功地让红黄绿灯进入了"无限循环"模式。不过,这种层层嵌套的回调函数,是不是让你想起了"回调地狱"(Callback Hell)?别急,接下来我们有更好的解决方案!
✨ Promise 实现:告别"回调地狱"的救星
"回调地狱"让人头大,代码可读性差,维护起来更是噩梦。这时候,Promise 就闪亮登场了!Promise 是一种更优雅的异步编程解决方案,它能让你更好地管理异步操作的流程。
首先,我们需要改造一下 task
函数,让它不再接受 callback
参数,而是返回一个 Promise 对象:
javascript
const task = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red();
} else if (light === 'green') {
green();
} else if (light === 'yellow') {
yellow();
}
resolve(); // 亮灯结束后,调用 resolve() 表示任务完成
}, timer);
});
};
const step = () => {
task(3000, 'red') // 红灯亮3秒
.then(() => task(2000, 'green')) // 接着绿灯亮2秒
.then(() => task(1000, 'yellow')) // 然后黄灯亮1秒
.then(step); // 再次调用 step,实现循环
};
step(); // 启动 Promise 版本的红绿灯循环!
看,是不是清爽多了? 通过 Promise
的链式调用 .then()
,我们把异步操作的流程扁平化了,避免了层层嵌套。每个 task
调用都会返回一个 Promise,当前一个 task
完成后(即 resolve()
被调用),.then()
中的下一个 task
就会被执行。最后,我们依然在链的末尾调用 step
函数,实现了红黄绿灯的无限循环。这就像是把一根长长的线团,整理成了一串清晰的珠链,一目了然!
🚀 Async/Await 实现:异步编程的"终极武器"
如果说 Promise 让你告别了"回调地狱",那么 async/await
简直就是前端异步编程的"终极武器"!它能让你用同步的思维去编写异步代码,让代码逻辑更加清晰,可读性更强,简直是"妈妈再也不用担心我写异步代码了"系列!
javascript
const taskRunner = async () => {
await task(3000, 'red'); // 等待红灯亮3秒
await task(2000, 'green'); // 等待绿灯亮2秒
await task(1000, 'yellow'); // 等待黄灯亮1秒
taskRunner(); // 再次调用自身,实现循环
};
taskRunner(); // 启动 Async/Await 版本的红绿灯循环!
是不是感觉代码像在讲故事? await
关键字会暂停 taskRunner
函数的执行,直到它后面的 Promise 被解决(也就是对应的灯亮完)。这样,红灯亮完再亮绿灯,绿灯亮完再亮黄灯,一切都变得那么自然,就像你在读一本按顺序执行的说明书。最后,我们再次调用 taskRunner()
,就实现了无限循环亮灯的效果。这种写法,简直是把异步代码写出了同步代码的"丝滑感"!
总结:前端异步编程的"三驾马车"
通过这个"红黄绿灯"的小例子,我们对比了前端异步编程的三种常见实现方式:
- Callback (回调函数):最原始的异步处理方式,简单直接,但容易陷入"回调地狱",代码可读性和维护性较差。
- Promise (承诺):对回调函数的一种改进,通过链式调用解决了"回调地狱"的问题,让异步流程更加清晰。
- Async/Await (异步/等待):基于 Promise 的语法糖,用同步的写法处理异步操作,极大地提高了代码的可读性和可维护性,是现代前端开发的首选。
每种方式都有其特点和适用场景,但毫无疑问,async/await
以其优雅的语法和强大的可读性,成为了现代前端开发的首选。希望这篇博客能让你对前端异步编程有更深入的理解,下次再遇到类似的面试题,你就能轻松应对啦!
祝你在前端的道路上,一路绿灯! 🚀