用 Promise 实现可取消的红绿灯循环,怎样避免递归和定时器泄漏?
先理解原理和示例,再练习完整口述与追问。
回答要点
等待包装 Promise;for/await 顺序;AbortSignal;取消清定时器与监听;有限轮测试;显示与停留语义明确。
原理拆解
题目先约定为“立即显示红灯保持 3 秒,再绿灯 2 秒,再黄灯 1 秒,循环”。Promise 负责表示一次等待,async 循环顺序执行,而不是同步无限递归。取消必须使等待中的 Promise 结束,清除定时器,并移除不再需要的监听,否则外层任务会挂起或泄漏。
示例与推演
function abortError() {
return Object.assign(new Error('任务已取消'), { name: 'AbortError' })
}
function wait(ms, signal) {
if (!Number.isFinite(ms) || ms < 0) return Promise.reject(new RangeError('时间不合法'))
return new Promise((resolve, reject) => {
if (signal?.aborted) { reject(abortError()); return }
let timer
function cancel() {
clearTimeout(timer)
signal?.removeEventListener('abort', cancel)
reject(abortError())
}
timer = setTimeout(() => {
signal?.removeEventListener('abort', cancel)
resolve()
}, ms)
signal?.addEventListener('abort', cancel, { once: true })
})
}
async function runLights(show, {
signal, rounds = Infinity,
schedule = [['red', 3000], ['green', 2000], ['yellow', 1000]]
} = {}) {
if (typeof show !== 'function') throw new TypeError('show 必须是函数')
if (rounds !== Infinity && (!Number.isInteger(rounds) || rounds < 0)) throw new RangeError('轮数不合法')
if (!Array.isArray(schedule) || !schedule.length || !schedule.every(item =>
Array.isArray(item) && item.length === 2 && typeof item[0] === 'string'
&& Number.isFinite(item[1]) && item[1] >= 0)) throw new TypeError('灯序列不合法')
for (let round = 0; round < rounds; round++) {
for (const [color, duration] of schedule) {
if (signal?.aborted) throw abortError()
show(color)
await wait(duration, signal)
}
}
}
业务持有 AbortController,卸载时 abort;调用者 catch AbortError 区分主动停止与真实失败。测试使用两轮和短 schedule,验证 red→green→yellow 重复,而不是让测试永久等待。每一步只保留必要状态,循环次数增加不应累积同步调用栈;show 在此约定为同步动作。
易错点
定时器时长是最低调度门槛,不保证真实秒表准确。若目标是按绝对时间表补偿漂移,需要另记录基准时间;不能一边要求持续时间一边偷偷跳过灯态。
参考回答可直接练习口述
我把等待、显示与循环分开,用 await 保证顺序,用 AbortSignal 终止等待并回收计时器与监听。明确立即显示后停留的语义,支持有限轮测试,区分取消和异常,也说明定时调度误差。