面試精選之Promise

lucefer發表於2018-06-26

前端面試過程中,基本都會問到 Promise,如果你足夠幸運,面試官問的比較淺,僅僅問 Promise 的使用方式,那麼恭喜你。事實上,大多數人並沒有那麼幸運。所以,我們要準備好九淺一深的知識。

不知道讀者有沒有想過,為什麼那麼多面試官都喜歡問Promise?可以思考一下哦~

常見 Promise 面試題

我們看一些 Promise 的常見面試問法,由淺至深。

  • 1、瞭解 Promise 嗎?
  • 2、Promise 解決的痛點是什麼?
  • 3、Promise 解決的痛點還有其他方法可以解決嗎?如果有,請列舉。
  • 4、Promise 如何使用?
  • 5、Promise 常用的方法有哪些?它們的作用是什麼?
  • 6、Promise 在事件迴圈中的執行過程是怎樣的?
  • 7、Promise 的業界實現都有哪些?
  • 8、能不能手寫一個 Promise 的 polyfill。

這些問題,如果你都能 hold 住,那麼面試官基本認可你了。帶著上面這些問題,我們往下看。

Promise 出現的原因

在 Promise 出現以前,我們處理一個非同步網路請求,大概是這樣:


// 請求 代表 一個非同步網路呼叫。
// 請求結果 代表網路請求的響應。
請求1(function(請求結果1){
    處理請求結果1
})
複製程式碼

看起來還不錯。
但是,需求變化了,我們需要根據第一個網路請求的結果,再去執行第二個網路請求,程式碼大概如下:

請求1(function(請求結果1){
    請求2(function(請求結果2){
        處理請求結果2
    })
})
複製程式碼

看起來也不復雜。
但是。。需求是永無止境的,於是乎出現瞭如下的程式碼:

請求1(function(請求結果1){
    請求2(function(請求結果2){
        請求3(function(請求結果3){
            請求4(function(請求結果4){
                請求5(function(請求結果5){
                    請求6(function(請求結果3){
                        ...
                    })
                })
            })
        })
    })
})
複製程式碼

這回傻眼了。。。 臭名昭著的 回撥地獄 現身了。

更糟糕的是,我們基本上還要對每次請求的結果進行一些處理,程式碼會更加臃腫,在一個團隊中,程式碼 review 以及後續的維護將會是一個很痛苦的過程。

回撥地獄帶來的負面作用有以下幾點:

  • 程式碼臃腫。
  • 可讀性差。
  • 耦合度過高,可維護性差。
  • 程式碼複用性差。
  • 容易滋生 bug。
  • 只能在回撥裡處理異常。

出現了問題,自然就會有人去想辦法。這時,就有人思考了,能不能用一種更加友好的程式碼組織方式,解決非同步巢狀的問題。

let 請求結果1 = 請求1();
let 請求結果2 = 請求2(請求結果1); 
let 請求結果3 = 請求3(請求結果2); 
let 請求結果4 = 請求2(請求結果3); 
let 請求結果5 = 請求3(請求結果4); 
複製程式碼

類似上面這種同步的寫法。 於是 Promise 規範誕生了,並且在業界有了很多實現來解決回撥地獄的痛點。比如業界著名的 Qbluebirdbluebird 甚至號稱執行最快的類庫。

看官們看到這裡,對於上面的問題 2 和問題 7 ,心中是否有了答案呢。^_^

什麼是 Promise

Promise 是非同步程式設計的一種解決方案,比傳統的非同步解決方案【回撥函式】和【事件】更合理、更強大。現已被 ES6 納入進規範中。

程式碼書寫比較

還是使用上面的網路請求例子,我們看下 Promise 的常規寫法:

new Promise(請求1)
    .then(請求2(請求結果1))
    .then(請求3(請求結果2))
    .then(請求4(請求結果3))
    .then(請求5(請求結果4))
    .catch(處理異常(異常資訊))
複製程式碼

比較一下這種寫法和上面的回撥式的寫法。我們不難發現,Promise 的寫法更為直觀,並且能夠在外層捕獲非同步函式的異常資訊。

API

Promise 的常用 API 如下:

  • Promise.resolve(value)

類方法,該方法返回一個以 value 值解析後的 Promise 物件 1、如果這個值是個 thenable(即帶有 then 方法),返回的 Promise 物件會“跟隨”這個 thenable 的物件,採用它的最終狀態(指 resolved/rejected/pending/settled)
2、如果傳入的 value 本身就是 Promise 物件,則該物件作為 Promise.resolve 方法的返回值返回。
3、其他情況以該值為成功狀態返回一個 Promise 物件。

上面是 resolve 方法的解釋,傳入不同型別的 value 值,返回結果也有區別。這個 API 比較重要,建議大家通過練習一些小例子,並且配合上面的解釋來熟悉它。如下幾個小例子:

//如果傳入的 value 本身就是 Promise 物件,則該物件作為 Promise.resolve 方法的返回值返回。  
function fn(resolve){
    setTimeout(function(){
        resolve(123);
    },3000);
}
let p0 = new Promise(fn);
let p1 = Promise.resolve(p0);
// 返回為true,返回的 Promise 即是 入參的 Promise 物件。
console.log(p0 === p1);
複製程式碼

傳入 thenable 物件,返回 Promise 物件跟隨 thenable 物件的最終狀態。

ES6 Promises 裡提到了 Thenable 這個概念,簡單來說它就是一個非常類似 Promise 的東西。最簡單的例子就是 jQuery.ajax,它的返回值就是 thenable 物件。但是要謹記,並不是只要實現了 then 方法就一定能作為 Promise 物件來使用。

//如果傳入的 value 本身就是 thenable 物件,返回的 promise 物件會跟隨 thenable 物件的狀態。
let promise = Promise.resolve($.ajax('/test/test.json'));// => promise物件
promise.then(function(value){
   console.log(value);
});
複製程式碼

返回一個狀態已變成 resolved 的 Promise 物件。

let p1 = Promise.resolve(123); 
//列印p1 可以看到p1是一個狀態置為resolved的Promise物件
console.log(p1)
複製程式碼
  • Promise.reject

類方法,且與 resolve 唯一的不同是,返回的 promise 物件的狀態為 rejected。

  • Promise.prototype.then

例項方法,為 Promise 註冊回撥函式,函式形式:fn(vlaue){},value 是上一個任務的返回結果,then 中的函式一定要 return 一個結果或者一個新的 Promise 物件,才可以讓之後的then 回撥接收。

  • Promise.prototype.catch

例項方法,捕獲異常,函式形式:fn(err){}, err 是 catch 註冊 之前的回撥丟擲的異常資訊。

  • Promise.race

類方法,多個 Promise 任務同時執行,返回最先執行結束的 Promise 任務的結果,不管這個 Promise 結果是成功還是失敗。 。

  • Promise.all

類方法,多個 Promise 任務同時執行。
如果全部成功執行,則以陣列的方式返回所有 Promise 任務的執行結果。 如果有一個 Promise 任務 rejected,則只返回 rejected 任務的結果。

  • ...

以上幾種便是 Promise 的常用 API,掌握了這些,我們便可以熟練使用 Promise了。

一定要多練習,熟練掌握,否則一知半解的理解在面試時捉襟見肘。

如何理解 Promise

為了便於理解 Promise,大家除了要多加練習以外,最好的方式是能夠將Promise的機制與現實生活中的例子聯絡起來,這樣才能真正得到消化。

我們可以把 Promise 比作一個保姆,家裡的一連串的事情,你只需要吩咐給他,他就能幫你做,你就可以去做其他事情了。
比如,作為一家之主的我,某一天要出門辦事,但是我還要買菜做飯送到老婆單位(請理解我在家裡的地位。。)

出門辦的事情很重要,買菜做飯也重要。。但我自己只能做一件事。

這時我就可以把買菜做飯的事情交給保姆,我會告訴她:

  • 你先去超市買菜。
  • 用超市買回來的菜做飯。
  • 將做好的飯菜送到老婆單位。
  • 送到單位後打電話告訴我。

我們知道,上面三步都是需要消耗時間的,我們可以理解為三個非同步任務。利用 Promise 的寫法來書寫這個操作:

function 買菜(resolve,reject) {
    setTimeout(function(){
        resolve(['蕃茄''雞蛋''油菜']);
    },3000)
}
function 做飯(resolve, reject){
    setTimeout(function(){
        //對做好的飯進行下一步處理。
        resolve ({
            主食: '米飯',
            菜: ['蕃茄炒雞蛋''清炒油菜']
        })
    },3000) 
}
function 送飯(resolve,reject){
    //對送飯的結果進行下一步處理
    resolve('老婆的麼麼噠');
}
function 電話通知我(){
    //電話通知我後的下一步處理
    給保姆加100塊錢獎金;
}
複製程式碼

好了,現在我整理好了四個任務,這時我需要告訴保姆,讓他按照這個任務列表去做。這個過程是必不可少的,因為如果不告訴保姆,保姆不知道需要做這些事情。。(我這個保姆比較懶)

// 告訴保姆幫我做幾件連貫的事情,先去超市買菜
new Promise(買菜)
//用買好的菜做飯
.then((買好的菜)=>{
    return new Promise(做飯);
})
//把做好的飯送到老婆公司
.then((做好的飯)=>{
    return new Promise(送飯);
})
//送完飯後打電話通知我
.then((送飯結果)=>{
    電話通知我();
})
複製程式碼

至此,我通知了保姆要做這些事情,然後我就可以放心地去辦我的事情。

請一定要謹記:如果我們的後續任務是非同步任務的話,必須return 一個 新的 promise 物件。
如果後續任務是同步任務,只需 return 一個結果即可。
我們上面舉的例子,除了電話通知我是一個同步任務,其餘的都是非同步任務,非同步任務 return 的是 promise物件。

除此之外,一定謹記,一個 Promise 物件有三個狀態,並且狀態一旦改變,便不能再被更改為其他狀態。

  • pending,非同步任務正在進行。
  • resolved (也可以叫fulfilled),非同步任務執行成功。
  • rejected,非同步任務執行失敗。

Promise的使用總結。

Promise 這麼多概念,初學者很難一下子消化掉,那麼我們可以採取強制記憶法,強迫自己去記住使用過程。

  • 首先初始化一個 Promise 物件,可以通過兩種方式建立, 這兩種方式都會返回一個 Promise 物件。

    • 1、new Promise(fn)
    • 2、Promise.resolve(fn)
  • 然後呼叫上一步返回的 promise 物件的 then 方法,註冊回撥函式。

    • then 中的回撥函式可以有一個引數,也可以不帶引數。如果 then 中的回撥函式依賴上一步的返回結果,那麼要帶上引數。比如
        new Promise(fn)
        .then(fn1(value){
            //處理value
        })
    複製程式碼
  • 最後註冊 catch 異常處理函式,處理前面回撥中可能丟擲的異常。

通常按照這三個步驟,你就能夠應對絕大部分的非同步處理場景。用熟之後,再去研究 Promise 各個函式更深層次的原理以及使用方式即可。

看到這裡之後,我們便能回答上面的問題 4 和問題 5了。

Promsie 與事件迴圈

Promise在初始化時,傳入的函式是同步執行的,然後註冊 then 回撥。註冊完之後,繼續往下執行同步程式碼,在這之前,then 中回撥不會執行。同步程式碼塊執行完畢後,才會在事件迴圈中檢測是否有可用的 promise 回撥,如果有,那麼執行,如果沒有,繼續下一個事件迴圈。

關於 Promise 在事件迴圈中還有一個 微任務的概念(microtask),感興趣的話可以看我這篇關於nodejs 時間迴圈的文章 剖析nodejs的事件迴圈,雖然和瀏覽器端有些不同,但是Promise 微任務的執行時機相差不大。

Promise 的升級

ES6 出現了 generator 以及 async/await 語法,使非同步處理更加接近同步程式碼寫法,可讀性更好,同時異常捕獲和同步程式碼的書寫趨於一致。上面的列子可以寫成這樣:

(async ()=>{
    let 蔬菜 = await 買菜();
    let 飯菜 = await 做飯(蔬菜);
    let 送飯結果 = await 送飯(飯菜);
    let 通知結果 = await 通知我(送飯結果);
})();
複製程式碼

是不是更清晰了有沒有。需要記住的是,async/await也是基於 Promise 實現的,所以,我們仍然有必要深入理解 Promise 的用法。

結語

相信各位看官看到這裡也累了,同時限於篇幅,本文不再對手動實現 Promise 進行講解了,留待下一篇文章~

上面的內容希望讀者多做練習,吃透 Promise 的使用與原理,讓面試更加從容。

相關文章