“金三銀四,金九銀十”,都是要收穫的季節。面對各種面試題,各種概念、原理都要去記,挺枯燥的。本文是面向面試題和實際使用談一下Promise。 Promise是什麼? Promise是JS非同步編程中的重要概念,非同步抽象處理對象,是目前比較流行Javascript非同步編程解決方案之一。這句話說的很明白了 ...
“金三銀四,金九銀十”,都是要收穫的季節。面對各種面試題,各種概念、原理都要去記,挺枯燥的。本文是面向面試題和實際使用談一下Promise。
Promise是什麼?
Promise是JS非同步編程中的重要概念,非同步抽象處理對象,是目前比較流行Javascript非同步編程解決方案之一。這句話說的很明白了,Promise是一種用於解決非同步問題的思路、方案或者對象方式。在js中,經常使用非同步的地方是Ajax交互。比如在es5時代,jQuery的ajax的使用success來完成非同步的:
$.ajax({ url:'/xxx', success:()=>{}, error: ()=>{} })
這種方法可以清楚的讓讀代碼的人明白那一部分是Ajax請求成功的回調函數和失敗的回調函數。但是問題來了,當一次請求需要連續請求多個介面時,這段代碼仿佛進入了一團亂麻中:
// 第一次 $.ajax({ url:'/xxx', success:()=>{ // 第二次 $.ajax({ url:'/xxx', success:()=>{ // 第三次 $.ajax({ url:'/xxx', success:()=>{ // 可能還會有 }, error: ()=>{} }) }, error: ()=>{} }) }, error: ()=>{} })
也許因為success和error這兩個函數的存在,理解這段代碼會很簡單,但是當我們更改需求的時候,這將成為一個棘手的問題。這就是回調地獄。
當然,這是es5時代。當js這門語言發展到es6時代時,Promise的出現給非同步帶來了變革。Promise提供一個then,來為非同步提供回調函數:
$.ajax({ url:'/xxx', }).then( ()=>{ // 成功的回調 }, ()=>{ // 失敗的回調 })
而其先進之處則是,可以在then方法中繼續寫Promise對象並返回,然後繼續調用then來進行回調操作。
Promise的用法
說完了Promise是什麼,下麵讓我們研究一下Promise怎麼使用。首先,Promise是一個對象,因此,我們使用new的方式新建一個。然後給它傳一個函數作為參數,這個函數呢也有兩個參數,一個叫resolve(決定),一個叫reject(拒絕),這兩個參數也是函數。緊接著,我們使用then來調用這個Promise:
const fn = new Promise(function (resolve, reject) { setTimeout(()=>{ let num = Math.ceil(Math.random() * 10) // 假設num為7 if (num > 5) { resolve(num) //返回7 } else { reject(num) } },2000) }) fn.then((res)=>{ console.log(res) // 7 },(err)=>{ console.log(err) })
這就是最簡單的Promise的使用。假設2秒鐘之後生成隨機數為7,因此resolve回調函數運行,then走第一個函數,console.log(7)。假設2秒鐘之後生成隨機數為3,因此reject回調函數運行,then走第二個函數,console.log(3)。
那你可能說了,Promise要是就這點能耐也沒什麼大不了的啊?我們上面說了Promise的先進之處在於可以在then方法中繼續寫Promise對象並返回,然後繼續調用then來進行回調操作:
fn = new Promise(function (resolve, reject) { let num = Math.ceil(Math.random() * 10) if (num > 5) { resolve(num) } else { reject(num) } }) // 第一次回調 fn.then((res)=>{ console.log(`res==>${res}`) return new Promise((resolve,reject)=>{ if(2*res>15){ resolve(2*res) }else{ reject(2*res) } }) },(err)=>{ console.log(`err==>${err}`) }).then((res)=>{ // 第二次回調 console.log(res) },(err)=>{ console.log(`err==>${err}`) })
這就可以代替了上面類似es5時代的jQurey的success的嵌套式的回調地獄的產生,讓代碼清爽了許多。這裡的resolve就相當於以前的success。
Promise的原理
在Promise的內部,有一個狀態管理器的存在,有三種狀態:pending、fulfilled、rejected。
(1) promise 對象初始化狀態為 pending。
(2) 當調用resolve(成功),會由pending => fulfilled。
(3) 當調用reject(失敗),會由pending => rejected。
因此,看上面的的代碼中的resolve(num)其實是將promise的狀態由pending改為fulfilled,然後向then的成功回掉函數傳值,reject反之。但是需要記住的是註意promsie狀態 只能由 pending => fulfilled/rejected, 一旦修改就不能再變(記住,一定要記住,下麵會考到)。
當狀態為fulfilled(rejected反之)時,then的成功回調函數會被調用,並接受上面傳來的num,進而進行操作。promise.then方法每次調用,都返回一個新的promise對象 所以可以鏈式寫法(無論resolve還是reject都是這樣)。
Promise的幾種方法
then
then方法用於註冊當狀態變為fulfilled或者reject時的回調函數:
// onFulfilled 是用來接收promise成功的值 // onRejected 是用來接收promise失敗的原因 promise.then(onFulfilled, onRejected);
需要註意的地方是then方法是非同步執行的。
// resolve(成功) onFulfilled會被調用 const promise = new Promise((resolve, reject) => { resolve('fulfilled'); // 狀態由 pending => fulfilled }); promise.then(result => { // onFulfilled console.log(result); // 'fulfilled' }, reason => { // onRejected 不會被調用 }) // reject(失敗) onRejected會被調用 const promise = new Promise((resolve, reject) => { reject('rejected'); // 狀態由 pending => rejected }); promise.then(result => { // onFulfilled 不會被調用 }, reason => { // onRejected console.log(rejected); // 'rejected' })
catch
catch在鏈式寫法中可以捕獲前面then中發送的異常。
fn = new Promise(function (resolve, reject) { let num = Math.ceil(Math.random() * 10) if (num > 5) { resolve(num) } else { reject(num) } }) fn..then((res)=>{ console.log(res) }).catch((err)=>{ console.log(`err==>${err}`) })
其實,catch相當於then(null,onRejected),前者只是後者的語法糖而已。
resolve、reject
Promise.resolve 返回一個fulfilled狀態的promise對象,Promise.reject 返回一個rejected狀態的promise對象。
Promise.resolve('hello').then(function(value){ console.log(value); }); Promise.resolve('hello'); // 相當於 const promise = new Promise(resolve => { resolve('hello'); }); // reject反之
all
但從字面意思上理解,可能為一個狀態全部怎麼樣的意思,讓我看一下其用法,就可以看明白這個靜態方法:
var p1 = Promise.resolve(1), p2 = Promise.reject(2), p3 = Promise.resolve(3); Promise.all([p1, p2, p3]).then((res)=>{ //then方法不會被執行 console.log(results); }).catch((err)=>{ //catch方法將會被執行,輸出結果為:2 console.log(err); });
大概就是作為參數的幾個promise對象一旦有一個的狀態為rejected,則all的返回值就是rejected。
當這幾個作為參數的函數的返回狀態為fulfilled時,至於輸出的時間就要看誰跑的慢了:
let p1 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('1s') //1s後輸出 resolve(1) },1000) }) let p10 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('10s') //10s後輸出 resolve(10) },10000) }) let p5 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('5s') //5s後輸出 resolve(5) },5000) }) Promise.all([p1, p10, p5]).then((res)=>{ console.log(res); // 最後輸出 })
這段代碼運行時,根據看誰跑的慢的原則,則會在10s之後輸出[1,10,5]。over,all收工。
race
promise.race()方法也可以處理一個promise實例數組但它和promise.all()不同,從字面意思上理解就是競速,那麼理解起來上就簡單多了,也就是說在數組中的元素實例那個率先改變狀態,就向下傳遞誰的狀態和非同步結果。但是,其餘的還是會繼續進行的。
let p1 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('1s') //1s後輸出 resolve(1) },1000) }) let p10 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('10s') //10s後輸出 resolve(10) //不傳遞 },10000) }) let p5 = new Promise((resolve)=>{ setTimeout(()=>{ console.log('5s') //5s後輸出 resolve(5) //不傳遞 },5000) }) Promise.race([p1, p10, p5]).then((res)=>{ console.log(res); // 最後輸出 })
因此,在這段代碼的結尾我們的結果為
1s 1 5s 10s
我們可以根據race這個屬性做超時的操作:
//請求某個圖片資源 let requestImg = new Promise(function(resolve, reject){ var img = new Image(); img.onload = function(){ resolve(img); } }); //延時函數,用於給請求計時 let timeOut = new Promise(function(resolve, reject){ setTimeout(function(){ reject('圖片請求超時'); }, 5000); }); Promise.race([requestImg, timeout]).then((res)=>{ console.log(res); }).catch((err)=>{ console.log(err); });
Promise相關的面試題
1.
const promise = new Promise((resolve, reject) => { console.log(1); resolve(); console.log(2); }); promise.then(() => { console.log(3); }); console.log(4);
輸出結果為:1,2,4,3。
解題思路:then方法是非同步執行的。
2.
const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve('success') reject('error') }, 1000) }) promise.then((res)=>{ console.log(res) },(err)=>{ console.log(err) })
輸出結果:success
解題思路:Promise狀態一旦改變,無法在發生變更。
3.
Promise.resolve(1) .then(2) .then(Promise.resolve(3)) .then(console.log)
輸出結果:1
解題思路:Promise的then方法的參數期望是函數,傳入非函數則會發生值穿透。
4.
setTimeout(()=>{ console.log('setTimeout') }) let p1 = new Promise((resolve)=>{ console.log('Promise1') resolve('Promise2') }) p1.then((res)=>{ console.log(res) }) console.log(1)
輸出結果:
Promise1
1
Promise2
setTimeout
解題思路:這個牽扯到js的執行隊列問題,整個script代碼,放在了macrotask queue中,執行到setTimeout時會新建一個macrotask queue。但是,promise.then放到了另一個任務隊列microtask queue中。script的執行引擎會取1個macrotask queue中的task,執行之。然後把所有microtask queue順序執行完,再取setTimeout所在的macrotask queue按順序開始執行。(具體參考https://www.zhihu.com/question/36972010)
5.Promise.resolve(1) .then((res) => { console.log(res); return 2; }) .catch((err) => { return 3; }) .then((res) => { console.log(res); });
輸出結果:1 2
解題思路:Promise首先resolve(1),接著就會執行then函數,因此會輸出1,然後在函數中返回2。因為是resolve函數,因此後面的catch函數不會執行,而是直接執行第二個then函數,因此會輸出2。
6.
const promise = new Promise((resolve, reject) => { setTimeout(() => { console.log('開始'); resolve('success'); }, 5000); }); const start = Date.now(); promise.then((res) => { console.log(res, Date.now() - start); }); promise.then((res) => { console.log(res, Date.now() - start); });
輸出結果:
開始
success 5002
success 5002
解題思路:promise 的.then
或者.catch
可以被調用多次,但這裡 Promise 構造函數只執行一次。或者說 promise 內部狀態一經改變,並且有了一個值,那麼後續每次調用.then
或者.catch
都會直接拿到該值。
7.
let p1 = new Promise((resolve,reject)=>{ let num = 6 if(num<5){ console.log('resolve1') resolve(num) }else{ console.log('reject1') reject(num) } }) p1.then((res)=>{ console.log('resolve2') console.log(res) },(rej)=>{ console.log('reject2') let p2 = new Promise((resolve,reject)=>{ if(rej*2>10){ console.log('resolve3') resolve(rej*2) }else{ console.log('reject3') reject(rej*2) } })
return p2 }).then((res)=>{ console.log('resolve4') console.log(res) },(rej)=>{ console.log('reject4') console.log(rej) })
輸出結果:
reject1
reject2
resolve3
resolve4
12
解題思路:我們上面說了Promise的先進之處在於可以在then方法中繼續寫Promise對象並返回。
8.重頭戲!!!!實現一個簡單的Promisefunction Promise(fn){ var status = 'pending' function successNotify(){ status = 'fulfilled'//狀態變為fulfilled toDoThen.apply(undefined, arguments)//執行回調 } function failNotify(){ status = 'rejected'//狀態變為rejected toDoThen.apply(undefined, arguments)//執行回調 } function toDoThen(){ setTimeout(()=>{ // 保證回調是非同步執行的 if(status === 'fulfilled'){ for(let i =0; i< successArray.length;i ++) { successArray[i].apply(undefined, arguments)//執行then裡面的回掉函數 } }else if(status === 'rejected'){ for(let i =0; i< failArray.length;i ++) { failArray[i].apply(undefined, arguments)//執行then裡面的回掉函數 } } }) } var successArray = [] var failArray = [] fn.call(undefined, successNotify, failNotify) return { then: function(successFn, failFn){ successArray.push(successFn) failArray.push(failFn) return undefined // 此處應該返回一個Promise } } }
解題思路:Promise中的resolve和reject用於改變Promise的狀態和傳參,then中的參數必須是作為回調執行的函數。因此,當Promise改變狀態之後會調用回調函數,根據狀態的不同選擇需要執行的回調函數。
總結
首先,Promise是一個對象,如同其字面意思一樣,代表了未來某時間才會知道結果的時間,不受外界因素的印象。Promise一旦觸發,其狀態只能變為fulfilled或者rejected,並且已經改變不可逆轉。Promise的構造函數接受一個函數作為參數,該參數函數的兩個參數分別為resolve和reject,其作用分別是將Promise的狀態由pending轉化為fulfilled或者rejected,並且將成功或者失敗的返回值傳遞出去。then有兩個函數作為Promise狀態改變時的回調函數,當Promise狀態改變時接受傳遞來的參數並調用相應的函數。then中的回調的過程為非同步操作。catch方法是對.then(null,rejectFn)的封裝(語法糖),用於指定發生錯誤時的回掉函數。一般來說,建議不要再then中定義rejected狀態的回調函數,應該使用catch方法代替。all和race都是競速函數,all結束的時間取決於最慢的那個,其作為參數的Promise函數一旦有一個狀態為rejected,則總的Promise的狀態就為rejected;而race結束的時間取決於最快的那個,一旦最快的那個Promise狀態發生改變,那個其總的Promise的狀態就變成相應的狀態,其餘的參數Promise還是會繼續進行的。
當然在es7時代,也出現了await/async的非同步方案,這會是我們以後談論的。