怎样用 Promise 实现红绿灯循环和 LazyMan 这类按顺序延时执行的题?
这类题考的是把“等一段时间再做下一件事”串起来。先把 setTimeout 包成返回 Promise 的 sleep,再用 async/await 在循环里依次等待;LazyMan 这种链式调用,则先把每个方法要做的事放进任务队列,等同步代码跑完后再按顺序执行,sleepFirst 就是把任务插到队头。
简答
红绿灯题是:红灯亮 3 秒,绿灯亮 2 秒,黄灯亮 1 秒,然后一直循环。直接嵌套
setTimeout能写出来,但层层回调很难读,也没法无限循环。更好的做法是先写一个sleep(ms),它返回一个在ms毫秒后完成的 Promise,然后在async函数里用while (true)循环,依次await sleep(3000)、await sleep(2000)、await sleep(1000)。LazyMan 题要求支持
LazyMan('Tom').eat('午饭').sleep(1000).eat('晚饭')这样的链式调用,而且sleep要真的让后面的输出等一会儿。难点在于链式调用是同步执行完的,每个方法只能返回this,不能自己等。所以每个方法只负责把要做的事包成一个任务函数,放进队列;构造时安排一个异步的“开始”,等整条链都调用完,再从队头逐个取出任务执行。
sleepFirst要求不管写在哪,都最先执行,它只需要把任务插到队列开头而不是末尾。这就是为什么要等同步代码全部执行完再开始:只有那时,队列里的任务才是完整的。要注意,开始执行的时机要用
setTimeout或queueMicrotask放到当前同步代码之后。如果在构造函数里立刻开始执行,后面链式调用加进来的任务就可能错过。
红绿灯
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function trafficLight(log, rounds = Infinity) {
const lights = [
['红', 3000],
['绿', 2000],
['黄', 1000],
];
for (let round = 0; round < rounds; round++) {
for (const [color, ms] of lights) {
log(color);
await sleep(ms);
}
}
}
await sleep(ms) 做了什么?sleep 返回的 Promise 在 ms 毫秒后才完成,await 让 async 函数暂停在这里,把控制权还给事件循环,时间到了再从下一行继续。所以这段代码虽然写成了普通的循环,实际是一个灯亮完再亮下一个。
rounds 参数是为了测试和演示方便,默认无限循环。真正的页面里,组件卸载时要能停下来,可以在循环条件里检查一个停止标记,或者用 AbortController 取消。
LazyMan
class LazyMan {
#tasks = [];
constructor(name, log) {
this.log = log;
this.#tasks.push(() => this.log(`我是 ${name}`));
// 等整条链式调用同步执行完,再开始按队列执行
setTimeout(() => this.#run(), 0);
}
async #run() {
for (const task of this.#tasks) await task();
}
eat(food) {
this.#tasks.push(() => this.log(`吃 ${food}`));
return this;
}
sleep(ms) {
this.#tasks.push(() => new Promise((resolve) => setTimeout(resolve, ms)));
return this;
}
sleepFirst(ms) {
this.#tasks.unshift(() => new Promise((resolve) => setTimeout(resolve, ms)));
return this;
}
}
const logs = [];
new LazyMan('Tom', (text) => logs.push(text)).eat('午饭').sleep(1000).eat('晚饭').sleepFirst(500);
执行顺序是:等 500 毫秒,输出“我是 Tom”“吃 午饭”,再等 1000 毫秒,输出“吃 晚饭”。
为什么 sleepFirst 写在最后,却最先执行?链式调用是同步代码,new LazyMan(...) 到 .sleepFirst(500) 一口气执行完,这期间只是往 #tasks 里放任务:先是“我是 Tom”,然后“吃 午饭”、等 1000、“吃 晚饭”,最后 sleepFirst 用 unshift 把等待 500 放到了最前面。直到这时,构造函数里的 setTimeout 回调才执行 #run,按队列顺序跑。
#run 里的 await task() 为什么对普通任务也能用?log 返回 undefined,await undefined 会立即继续;sleep 任务返回 Promise,await 就会等它完成。所以同一个循环能同时处理同步任务和异步任务。
常见追问
不用 async/await 怎么写?
用
reduce把任务串成一条 Promise 链:tasks.reduce((chain, task) => chain.then(task), Promise.resolve())。每个then等上一个任务完成,返回 Promise 的任务会让链等待它。
怎样限制同时执行的异步任务数?
这是并发控制,和按顺序执行不同,要维护一个“正在执行”的计数,完成一个再补一个,见《怎样实现有明确失败语义的异步并发池?》。