說在前面
說實話,剛開始在聽到這個面試題的時候,我是詫異的,紅綠燈?這不是單片機、FPGA、F28335、PLC的實驗嗎?!
而且還要用Promise去寫,當時我確實沒思路,只好硬着頭皮去寫,下來再review的時候,才真正懂了Promise紅綠燈的實現原理
下來我就由淺至深的分析Promise紅綠燈的實現原理
下面我就不講promise的原理和特點了,想具體看了解的可以看阮一峰老師的教程
主要說下紅綠燈用到promise最核心的一點就是 “promise實例的狀態變為Resolved,就會觸發then方法綁定的回調函數”
我是在做這個demo途中才徹底理解了這句話的真正含義。
簡單實現
用文字綠燈、黃燈、紅燈來模擬表示紅綠燈
function timeout(){ return new Promise(function(resolve,reject){ setTimeout(resolve,1000,"綠燈");
} function timeout2(){ return new Promise(function(resolve,reject){ setTimeout(resolve,2000,"黃燈"); }) } function timeout3(){ return new Promise(function(resolve,reject){ setTimeout(resolve,3000,"紅燈"); }) } (function restart(){ timeout().then((value)=>{ console.log(value); }) timeout2().then((value)=>{ console.log(value); }) timeout3().then((value)=>{ console.log(value); restart(); }) })()
建立三個promise對象,分別用timeout1 timeout2 timeout3 包起來,promise對象里面含有定時器setTimeout,以連續的1000-》2000-》3000的時間表示每次燈亮的時間的為1秒
下面是實現的demo效果

這種實現有一個問題,如果設定綠燈是5000ms,黃燈是2000ms,紅燈是3000ms,
則會出現先顯示黃燈,后顯示紅燈,顯示綠燈的同時也會同時顯示黃燈,
因為第二輪綠燈的5000ms包含了黃燈的2000ms
這就不符合紅綠燈的思想與邏輯
較復雜實現
針對上一個問題,所以有了第二種解決方案
function green(){ return new Promise(function(resolve,reject){ console.log("綠燈"+new Date().getSeconds()) resolve(); }) } function yellow(){ return new Promise(function(resolve,reject){ console.log("黃燈"+new Date().getSeconds()) resolve(); }) } function red(){ return new Promise(function(resolve,reject){ console.log("紅燈"+new Date().getSeconds()) resolve(); }) } function ms_5000(){ return new Promise(function(resolve,reject){ setTimeout(resolve,5000) }) } function ms_3000(){ return new Promise(function(resolve,reject){ setTimeout(resolve,3000) }) } function ms_2000(){ return new Promise(function(resolve,reject){ setTimeout(resolve,2000) }) } (function restart(){ green() .then(ms_5000) //綠燈顯示5s轉紅燈 .then(yellow) .then(ms_3000) //黃燈顯示3s轉紅燈 .then(red) .then(ms_2000) //紅燈顯示2s轉綠燈
.then(arguments.callee)
})()
建立三個promise對象 分別用green yellow red 函數包起來,並返回promise對象的resolve,promise對象的狀態變成Resolved 也就是說return了reslove就可以可以觸發then方法綁定的回調函數
又建立了三個定時器,用於延時,三個定時器中用到了resolve函數,resolve是js引擎自帶的函數,也表示promise的狀態變成了Resolved,可以觸發then方法綁定的回調函數。
實現的demo如下,demo的數字是時間戳,當前的秒數,綠燈55 黃燈0 表示綠燈執行5秒后轉到黃燈,下面的同理
但是這樣做還是有點麻煩,代碼復用率低,要建立3個promise對象,3個定時器,無疑是消耗內存的。
倒數第2行 arguments.callee的含義下面也會解釋。
較復雜實現(理理思路)
function green(){ return new Promise(function(resolve,reject){ console.log("綠燈當前秒數"+new Date().getSeconds()) resolve(); }) } (function restart(){ green().then(function(){ return new Promise(function(resolve,reject){ setTimeout(resolve,5000); }) }).then(function(){ return new Promise(function(resolve,reject){ console.log("黃燈當前秒數" + new Date().getSeconds()) resolve(); }) }).then(function(){ return new Promise(function(resolve,reject){ setTimeout(resolve,3000); }) }).then(function(){ return new Promise(function(resolve,reject){ console.log("綠燈當前秒數"+ new Date().getSeconds()) resolve(); }) }).then(function(){ return new Promise(function(resolve,reject){ setTimeout(resolve,2000) }) }).then(arguments.callee); })()
上述的代碼功能和第2點相同,只是為了理理思路,體現出promise的狀態變成Resolved時,可以觸發then方法綁定的回調函數
就像上述代碼所示,執行紅綠燈的顯示,每次都會返回resolve,或者定時器也會使用resolve函數,表示promise的狀態確實變成Resolved了。promise有三種狀態pending fullfilled rejected ,pending到fulfilled表示的就是Resolved。
demo如下
完美實現(實現架構)
正如上面所說,上述的方法要建立3個promise對象,代碼復用率低,那有沒有更加嚴(gao)格(duan)的的方法,答案是有的,但是在看寫代碼前需要理理思路,分析一下代碼的架構如何去寫
function green2yellow2red(){ return function(){ // someCode return new Promise(function(){ // someCode }) } } var green = green2red2yellow(setTimeout).bind(null, 3000); var yellow = green2red2yellow(setTimeout).bind(null, 4000); var red = green2red2yellow(setTimeout).bind(null, 5000); (function(){ // IIFE green() })()
上述代碼使用一個promise對象,用green2yellow2red函數包起來,有兩個return,第一個return是為了給第二個return的promise對象bind延遲時間,bind綁定的參數可以通過arguments訪問到,必須是第一個return的函數中的arguments,arguments是什么下面也會講。
下面打印好多參數,下面我詳細解釋一下他們的區別
function green2red2yellow(){ console.log(arguments) // [ƒ, callee: ƒ, Symbol(Symbol.iterator): ƒ] // [ƒ, callee: ƒ, Symbol(Symbol.iterator): ƒ] // [ƒ, callee: ƒ, Symbol(Symbol.iterator): ƒ] console.log(this); // Window {stop: ƒ, open: ƒ, alert: ƒ, confirm: ƒ, prompt: ƒ, …} // Window {stop: ƒ, open: ƒ, alert: ƒ, confirm: ƒ, prompt: ƒ, …} // Window {stop: ƒ, open: ƒ, alert: ƒ, confirm: ƒ, prompt: ƒ, …} return function(){ console.log(arguments) // [3000, callee: ƒ, Symbol(Symbol.iterator): ƒ] console.log(this); // Window {stop: ƒ, open: ƒ, alert: ƒ, confirm: ƒ, prompt: ƒ, …} console.log(arguments.callee.length) // 形參的個數 console.log(arguments.length) // 實參的個數 var arr = []; var arr2 =[].slice.call(arguments); //把arguments類數組轉成真數組 console.log(arguments[0]) //3000 type是Number console.log(arr.push(arguments)) //返回1表示當前代碼執行結果為真 console.log(arr); //[Arguments(1)] console.log(arr2) // [3000] type是Array return new Promise(function(){ console.log(arguments) // (2)[ƒ, ƒ, callee: ƒ, Symbol(Symbol.iterator): ƒ] // 實參的類數組 }) } } var green = green2red2yellow(setTimeout).bind(null, 3000); var yellow = green2red2yellow(setTimeout).bind(null, 4000); var red = green2red2yellow(setTimeout).bind(null, 5000); (function(){ // IIFE green() })() //測試代碼段 var promise = new Promise(function(){ console.log(arguments) // (2)[ƒ, ƒ, callee: ƒ, Symbol(Symbol.iterator): ƒ] })
在green2red2yellow函數中直接console.log(arguments),打印出來三個數組,是因為實例了三次promise對象,分別是green,yellow,red,三次都指向同一個對象,所以打印了三次。
在green2red2yellow函數中的第一個return中console.log(arguments),只打印在IIFE中執行的的promise對象,就是green對象
在green2red2yellow函數中的第二個return中console.log(arguments),顯示結果前面有一個2表示,實參的個數是2
在測試代碼段中測試了一下,確實是。
arguments:以類數組的方式存放着當前對象的實參。green2red2yellow函數中訪問是green2red2yellow這個函數對象,green2red2yellow函數中第一個return中訪問是return的function bind了參數的的對象,green2red2yellow函數中第二個return是promise對象
arguments.callee:正在執行的這個函數的引用
arguments.callee.length:當前對象形參的個數
arguments.length:當前對象實參的個數
如何把arguments這個類數組轉換成數組呢:[ ].slice.call(arguments) 這是最穩妥的方法
下面的使用兩種方式把arguments轉成數組及兩者的區別
var arr = []; var arr2 =[].slice.call(arguments); //把arguments類數組轉成真數組 console.log(arguments[0]) //3000 type是Number console.log(arr.push(arguments)) //返回1表示當前代碼執行結果為真 console.log(arr); //[Arguments(1)] console.log(arr2) // [3000] type是Array
由此可知 [ ].slice.call(arguments)是最穩妥的方式
完美實現
下面寫出我覺得最完美的的實現方式
html:
<ul id="traffic" class=""> <li id="green"></li> <li id="yellow"></li> <li id="red"></li> </ul>
css:
/*垂直居中*/ ul {position: absolute;width: 200px;height: 200px;top: 50%;left: 50%;transform: translate(-50%,-50%);} /*畫3個圓代表紅綠燈*/ ul >li {width: 40px;height: 40px;border-radius:50%;opacity: 0.2;display: inline-block;} /*執行時改變透明度*/ ul.red >#red, ul.green >#green,ul.yellow >#yellow{opacity: 1.0;} /*紅綠燈的三個顏色*/ #red {background: red;} #yellow {background: yellow;} #green {background: green;}
JS:
function green2red2yellow(timer){ return function(){ var arr = [].slice.apply(arguments) // var self = this; return new Promise(function(resolve,reject){ arr.unshift(resolve) timer.apply(self,arr); }) } }
var green = green2red2yellow(setTimeout).bind(null, 3000); var yellow = green2red2yellow(setTimeout).bind(null, 4000); var red = green2red2yellow(setTimeout).bind(null, 5000); var traffic = document.getElementById("traffic");
(function restart(){ 'use strict' //嚴格模式 console.log("綠燈"+new Date().getSeconds()) //綠燈執行三秒 traffic.className = 'green'; green() .then(function(){ console.log("黃燈"+new Date().getSeconds()) //黃燈執行四秒 traffic.className = 'yellow'; return yellow(); }) .then(function(){ console.log("紅燈"+new Date().getSeconds()) //紅燈執行五秒 traffic.className = 'red'; return red(); }).then(function(){ restart() }) })();
1、var arr = [].slice.apply(arguments)
表示把arguments轉成數組
2、arr.unshift(resolve)
unshift或shift 在數組首項插入某值或刪除首項 push pop 是在數組尾部操作
3、timer.apply(self,arr);
timer是形參,引用了定時器setTimeout,apply是改變this的指向並可以數組的形式傳入參數作為
定時器執行的形參,定時器this的指向為self
self就是this就是window,等價於 timer(arr[0],arr[1]);
4、'use strict'
嚴格模式,在嚴格模式下 arguments.callee(正在執行的這個函數的引用)無效
5、restart()
遞歸
6、promise的狀態變成Resolved,就會觸發then綁定的回調函數,
所以每次then都是return一個promsise對象,因為在promise對象中狀態變成了Resolved
下面是實現的demo
注意:立即執行函數的script要寫入body里面,否則會顯示dom操作獲得元素為null,我剛踩到這個坑了...
總結
這個問題的解決讓我重新認識了promise,又重新認識了arguments,又重新認識了JS的強大。