面試 | 商湯科技面試經歷之Promise紅綠燈的實現

說在前面

  說實話,剛開始在聽到這個面試題的時候,我是詫異的,紅綠燈?這不是單片機、FPGA、F2833五、PLC的實驗嗎?!css

  並且還要用Promise去寫,當時我確實沒思路,只好硬着頭皮去寫,下來再review的時候,才真正懂了Promise紅綠燈的實現原理html

  下來我就由淺至深的分析Promise紅綠燈的實現原理前端

  下面我就不講promise的原理和特色了,想具體看了解的能夠看阮一峯老師的教程es6

  主要說下紅綠燈用到promise最核心的一點就是  「promise實例的狀態變爲Resolved,就會觸發then方法綁定的回調函數面試

  我是在作這個demo途中才完全理解了這句話的真正含義。數組

簡單實現

  用文字綠燈、黃燈、紅燈來模擬表示紅綠燈promise

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效果app


  這種實現有一個問題,若是設定綠燈是5000ms,黃燈是2000ms,紅燈是3000ms,
  則會出現先顯示黃燈,後顯示紅燈,顯示綠燈的同時也會同時顯示黃燈,
  由於第二輪綠燈的5000ms包含了黃燈的2000ms
  這就不符合紅綠燈的思想與邏輯
較複雜實現

  針對上一個問題,因此有了第二種解決方案dom

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() }) })();
一、var arr = [].slice.apply(arguments)  
表示把arguments轉成數組

二、arr.unshift(resolve)
unshift或shift 在數組首項插入某值或刪除首項 push pop 是在數組尾部操做

三、timer.apply(self,arr);        
timer是形參,引用了定時器setTimeout,apply是改變this的指向並能夠數組的形式傳入參數做爲
定時器執行的形參,定時器this的指向爲self
self就是this就是window,等價於 timer(arr[0],arr[1]);

四、'use strict'
嚴格模式,在嚴格模式下 arguments.callee(正在執行的這個函數的引用)無效

五、restart()
遞歸

六、promise的狀態變成Resolved,就會觸發then綁定的回調函數,
因此每次then都是return一個promsise對象,由於在promise對象中狀態變成了Resolved

下面是實現的demo
    
注意:當即執行函數的script要寫入body裏面,不然會顯示dom操做得到元素爲null,我剛踩到這個坑了...

總結

  這個問題的解決讓我從新認識了promise,又從新認識了arguments,又從新認識了JS的強大。

  紅綠燈大戰後,promise開發的最佳實踐是怎樣的?

  前端要給力之:紅綠燈大戰中的火星生命-Promise

  ECMAScript 6 入門(阮一峯)

相關文章
相關標籤/搜索