这是一道真实的字节跳动的面试题,通过这个可以加深对 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 (); while (Date .now () - start < ms) { } } function runBadTrafficLight ( ) { while (true ) { console .log ('红灯亮了' ); sleep (3000 ); console .log ('绿灯亮了' ); sleep (2000 ); console .log ('黄灯亮了' ); sleep (1000 ); } } 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 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 ();
这段代码有两个核心点:
使用 链式调用 完成”等待“需求。
每一个 .then 等会返回一个 promise ,下一个 .then 在上一个 .then 返回的 promise 敲定前(敲定指的是返回的 promise 得到 resolve 或者 reject ,而变成 fulfilled 或者 rejected 的状态)不会执行,而是一直等待上一个 .then() 返回的 promise 敲定。
通过这点,完美实现里上一个灯熄灭前下一个灯会一直等待的核心需求和难点。
使用递归的方式完成”循环进行永不停止“的需求
可见第三个 .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 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 ; } 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 { await this .lightUp ('红' , 3000 ); if (!this .isRunning ) break ; await this .lightUp ('绿' , 2000 ); if (!this .isRunning ) break ; 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 (); setTimeout (() => { trafficLight.stop (); }, 10000 ); console .log ('信号灯程序已启动' );
主要有三点升级:
创建了 delay 函数模拟灯的常亮,业务逻辑更分离。
使用了面向对象的编程设计,更内聚、更易于扩展。
每一段信号灯之间有一次停止的机会,不过信号灯中间不能停。
解法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 ; } }
这个版本实现了一个可中断的 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 也被敲定为停止函数了。
除此之外,灯的顺序和亮起时长也抽取成一个对象数组,这使得程序逻辑更清晰,更符合现代前端开发的惯例。