前端面試過程中,基本都會問到 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 規範誕生了,並且在業界有了很多實現來解決回撥地獄的痛點。比如業界著名的 Q 和 bluebird,bluebird 甚至號稱執行最快的類庫。
看官們看到這裡,對於上面的問題 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 的使用與原理,讓面試更加從容。