前端面試題之Promise問題
前言
在我們日常開發中會遇到很多異步的情況,比如涉及到
網絡請求(ajax,axios等)
,定時器
這些,對於這些異步操作我們如果需要拿到他們操作后的結果,就需要使用到回調函數。拿請求來說,如果我們需要拿到請求回來的數據我們就需要利用回調函數(見代碼片段1),以下所有的請求都是使用jQuery的ajax模擬。
點擊查看代碼片段1
// 代碼片段1
$.ajax({
url: 'url',
type: 'post',
data: {
參數1: 值1,
參數2: 值2
},
success: function(res){
// success就是響應成功的回調函數,在此處可以獲取到響應返回的內容
console.log(res)
}
})
在網絡請求中會遇到請求之前的依賴,一個請求會依賴於另一個請求的時候,就會需要出現回調嵌套的問題(代碼片段2)
點擊查看代碼片段2
// 代碼片段2
// 請求2的請求參數是請求1的響應結果
// 請求3的請求參數是請求2的響應結果
$.ajax({
url: '請求1的地址',
type: 'post',
data: {
參數1: 值1,
參數2: 值2
},
success: function(res){
// res是請求1的響應結果
console.log(res)
$.ajax({
url: '請求2的地址',
type: 'post',
data: {
// 參數1是請求1的響應結果
參數1: res,
參數2: 值2
},
success: function(res2){
// res2是請求2的響應結果
$.ajax({
url: '請求3的地址',
type: 'post',
data: {
// 參數1是請求2的響應結果
參數1: res2,
參數2: 值2
},
success: function(res3){
// res3是最終請求的結果
console.log(res3)
}
})
}
})
}
})
這樣的代碼就出現了js編程里面的著名
回調地獄問題
,為了解決這個問題我們需要利用es2015的promise和es2016的await來解決(代碼片段3)
點擊查看代碼片段3
// 代碼片段3
// await不能寫在同步的代碼里面,會阻塞整個程序的進程,只能寫在異步的代碼里面
// async可以修飾一個函數讓這個函數變成一個異步的函數
async function getRes(){
// $.ajax() 返回的是一個promise對象
// await可以等待一個promise狀態結束,拿到響應的結果
const res1 = await $.ajax({
url: '請求1的地址',
type: 'post',
data: {
參數1: 值1,
參數2: 值2
}
})
const res2 = await $.ajax({
url: '請求2的地址',
type: 'post',
data: {
參數1: res,
參數2: 值2
}
})
const res3 = await $.ajax({
url: '請求3的地址',
type: 'post',
data: {
參數1: res2,
參數2: 值2
}
})
}
// 調用異步函數
getRes()
利用promise和await就可以將之前的回調嵌套地獄改成同步的代碼方式,但是這里面大家要注意await的使用事項,await不允許出現在同步代碼塊里面,會阻塞同步代碼執行,必須寫在異步的代碼塊里面,加async讓getRes函數成為一個異步的函數,這樣getRes執行不會阻塞全局,getRes函數內部代碼就是一個同步的方式執行,就解決了回調嵌套的問題。本來故事到這里就該說goodBye了,但是現在很多面試,尤其是一些大廠的面試要求我們自己實現promise和await,這章節先給大家實現promise。
Promise的封裝
1. promise的介紹和使用
-
狀態(state)
- pending
- promise的初始狀態,並且此狀態可以轉換成
fulfilled
和rejected
- promise的初始狀態,並且此狀態可以轉換成
- fulfilled
- promise的成功狀態,不可以轉換其他狀態,並且必須有一個不可改變的最終值
value
- promise的成功狀態,不可以轉換其他狀態,並且必須有一個不可改變的最終值
- rejected
- promise的失敗狀態,不可以轉換其他狀態,並且必須有一個不可改變的原因
reason
- promise的失敗狀態,不可以轉換其他狀態,並且必須有一個不可改變的原因
- pending
-
術語
-
解決(fulfill)
當調用resolve方法時promise的狀態變成fulfill
-
拒絕(reject)
當調用reject方法時promise的狀態變成reject
-
終值(value)
所謂終值,指的是 promise 被解決時傳遞給解決回調的值,由於 promise 有一次性的特征,因此當這個值被傳遞時,標志着 promise 等待態的結束,故稱之終值,有時也直接簡稱為值(value)。
-
拒因(reason)
也就是拒絕(失敗)的原因,指在 promise 被拒絕時傳遞給拒絕回調的值。
-
-
面向對象編程方式
- Promise是面向對象編程方式,對應的構造函數是
Promise
- 使用Promise的時候需要創建一個實例對象,使用實例對象的方法
- Promise構造函數實例化的時候需要傳遞一個函數(handler),此函數里面可以接收兩個參數,一個函數叫做resolve,一個函數叫做reject
- 調用resolve時候promise實例的狀態變為 fulfilled
- 調用reject時候promise實例的狀態變為 rejected
- 此處要注意一個promise實例只有一個狀態,也就是不能同時調用resolve和reject
- Promise是面向對象編程方式,對應的構造函數是
點擊查看代碼
// promise01就是實例化Promise構造函數得到的實例對象
const promise01 = new Promise((resolve,reject)=>{
// 此處調用resolve時promise狀態會變成fulfilled
// 此處調用reject時promise狀態會變成rejected
})
- 實例對象的方法
- then方法
- 該方法有兩個參數,可選,分別對應onFulfilled,onRejected
- 當狀態為fulfilled時,調用onFulfilled,得到一個終值 value
- 當狀態為rejected時,調用onRejected,得到一個拒因 reason
- catch方法
- 該方法有一個參數,可選,對應onRejected
- 當狀態為rejected時,調用onRejected,得到一個拒因
- finally方法
- 該方法有一個參數,可選
- 該方法無論是promise狀態成功還是失敗都會調用
- 注意:
- 實例方法可以實現鏈式調用,因為每一次方法調用完成之后都會返回一個promise實例
- then方法可以調用多次
- then方法
點擊查看代碼
const promise01 = new Promise((resolve,reject)=>{
})
// 使用實例對象方法 鏈式調用
promise01.then(
// onFulfilled
function(value){
// 當promise實例的狀態為fulfilled調用
},
// onRejected
function(reason){
// 當promise實例的狀態為rejected調用
}
).then(
// then方法可以調用多次
// 並且調用可以不傳遞參數
).catch(function(reason){
// 當promise實例的狀態為rejected調用
}).finally(function(){
// 無論promise實例的狀態為fulfilled或者rejected都會調用
})
2. 實現手寫Promise
-
實現Promise的構造函數
- 構造函數需要參數,參數必須是一個函數類型 這里面我們叫做handler,這兒會對handler類型進行驗證,如果不是函數類型會拋出類型異常
- handler函數需要兩個形式參數 對應的是類自身的兩個方法
- resolve方法
- 該方法只能在promise狀態為pending的時候調用,如果promise已經有了狀態,則不允許調用
- 調用方法promise的狀態變為fulfilled
- 調用該方法會得到一個終值value
- 該方法可能異步調用
- 該方法里面會用到實例的this,在調用的時候需要改變this的指向
- reject方法
- 該方法只能在promise狀態為pending的時候調用,如果promise已經有了狀態,則不允許調用
- 調用方法promise的狀態變為rejected
- 調用該方法會得到一個拒因reason
- 該方法可能異步調用
- 該方法里面會用到實例的this,在調用的時候需要改變this的指向
- resolve方法
- 實例里面屬性
- value 終值
- reason 拒因
- state 狀態
- 狀態可以作為靜態的屬性,也可以作為實例的屬性
- 狀態會多次用於判斷,所以建議用常量保存
- 實例里面的方法
- then
- catch
- finally
- 類自身的方法
- resolve
- reject
// promise.js // 狀態會多次用於判斷,所以建議用常量保存 const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class Promise{ constructor(handler){ // 構造函數需要參數,參數必須是一個函數類型 這里面我們叫做handler,這兒會對handler類型進行驗證,如果不是函數類型會拋出類型異常 if(typeof handler !== 'function'){ throw new TypeError(`Promise構造函數的參數${handler}不是一個函數`) } this.state = PENDING // 用來存儲promise的狀態 初始化狀態為pending this.reason = undefined // 拒因 this.value = undefined // 終值 // resolve,reject方法里面會用到實例的this,在調用的時候需要改變this的指向 handler(this.resolve.bind(this), this.reject.bind(this)) } resolve(value){ // 該方法只能在promise狀態為pending的時候調用,如果promise已經有了狀態,則不允許調用 if(this.state !== PENDING) return // 調用方法promise的狀態變為fulfilled this.state = FULFILLED // 調用該方法會得到一個終值value this.value = value } reject(reason){ // 該方法只能在promise狀態為pending的時候調用,如果promise已經有了狀態,則不允許調用 if(this.state !== PENDING) return // 調用方法promise的狀態變為rejected this.state = REJECTED // 調用該方法會得到一個拒因reason this.reason = reason } }
// 試錯 const p = new Promise(1) console.log(p) // promise.js:9 Uncaught TypeError: Promise構造函數的參數1不是一個函數 // 正確操作 // 狀態成功 const p = new Promise((resolve, reject) => { resolve() }) console.log(p) // Promise {state: 'fulfilled', reason: undefined, value: undefined} // 狀態失敗 const p = new Promise((resolve, reject) => { reject() }) console.log(p) // Promise {state: 'rejected', reason: undefined, value: undefined}
-
實現實例then方法
-
then方法接受兩個參數,兩個參數類型是函數
- onFulfilled
- 需要對onFulfilled類型進行驗證,如果不是一個函數就設置為默認函數
function(value){return value}
,這個函數是獲取終值 - 該函數在promise狀態為fulfilled時候調用,傳入終值
- 需要對onFulfilled類型進行驗證,如果不是一個函數就設置為默認函數
- onRejected
- 需要對onRejected類型進行驗證,如果不是一個函數就設置為一個默認函數
function(err){throw err}
,這個函數是獲取拒因 - 該函數在promise狀態為rejected時候調用,傳入拒因
- 需要對onRejected類型進行驗證,如果不是一個函數就設置為一個默認函數
// promise.js const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class Promise{ constructor(handler){...} resolve(value){...} reject(reason){...} then(onFulfilled, onRejected){ // 參數可選要進行判斷 如果傳遞不是一個函數 默認一個函數 onFulfilled = typeof onFulfilled==='function' ? onFulfilled : value => value; onRejected = typeof onRejected==='function' ? onRejected : err => {throw err}; if(this.state === FULFILLED){ // 當promise狀態為fulfilled時候調用onFulfilled onFulfilled(this.value) } if(this.state === REJECTED){ // 當promise狀態為rejected時候調用onRejected onRejected(this.reason) } } }
// test.js // 成功狀態 const p = new Promise((resolve, reject) => { resolve('success') }) p.then( res => { console.log('調用了onFullfilled') console.log(res) }, err => { console.log('調用了onRejected') console.log(err) } ) // 控制台輸出 調用了onFullfilled sucess // 失敗狀態 const p = new Promise((resolve, reject) => { reject('error') }) p.then( res => { console.log(res) }, err => { console.log(err) } ) // 控制台輸出 調用了onRejected error // handler函數異步調用resolve const p = new Promise((resolve, reject) => { setTimeout(()=>{ resolve('success') },3000) }) p.then( res => { console.log('調用了onFullfilled') console.log(res) }, err => { console.log('調用了onRejected') console.log(err) } ) // 控制台沒有任何內容輸出 onFulfilled函數並沒有被調用
- 此時經過測試,目前的promise可以正常的調用,此時針對於同步的調用沒有問題,但是在
handler
里面resolve
和reject
大部分情況下都是異步調用的,比如說延遲3s調用,此時then里面的onFulfilled
和onRejected
是無法執行的,因為then函數在執行的時候promise的狀態是pending,onFulfilled
和onRejected
函數里面是做了判斷,沒有對pendng狀態進行處理,針對於pending狀態的需要處理
// promise.js // ... class Promise { // 用來存儲狀態成功的回調函數 // 用來存儲狀態失敗的回調函數 successCallBack = null errorCallBack = null constructor(handler) {...} resolve(value) { // ... // 異步調用的時候執行存儲的onFulfilled函數,傳入終值 this.successCallBack(this.value) } reject(reason) { // ... // 異步調用的時候執行存儲的onRejected函數,傳入拒因 this.errorCallBack(this.reason) } then(onFulfilled, onRejected) { // ... if (this.state === PENDING) { // 當resolve或者reject異步調用,then執行的時候promise狀態等待 // 將onFulfilled和onReject函數存儲起來 this.successCallBack = onFulfilled this.errorCallBack = onRejected } } }
// test.js // 異步調用resolve const p = new Promise((resolve, reject) => { setTimeout(()=>{ resolve('success') },3000) }) p.then( res => { console.log('調用了onFullfilled') console.log(res) }, err => { console.log('調用了onRejected') console.log(err) } ) // 3s后控制台輸出 調用了onFullfilled success
- 此時promise的handler就可以異步調用
resolve
或reject
,then里面可以獲取到終值或據因 - then函數支持多次調用,所以此時then的回調函數就不止一個,我們將之前的變量改成數組用來存儲then的回調函數
// promise.js // ... class Promise { // 用數組的方式存儲失敗和成功的回調函數 successCallBack = [] errorCallBack = [] constructor(handler) {...} resolve(value) { // ... // 遍歷數組將所有成功的函數執行 this.successCallBack.forEach(fn=> fn()) } reject(reason) { // ... // 遍歷數組將所有失敗的函數執行 this.errorCallBack.forEach(fn=>fn()) } then(onFulfilled, onRejected) { // ... if (this.state === PENDING) { // 當resolve或者reject異步調用,then執行的時候promise狀態等待 // 將onFulfilled和onReject函數存儲起來 this.successCallBack.push(()=>{ onFulfilled(this.value) }) this.errorCallBack.push(()=>{ onRejected(this.reason) }) } } }
- 測試調用了兩次then,狀態成功之后兩次then的成功回調都會被執行
// test.js // 異步調用resolve const p = new Promise((resolve, reject) => { setTimeout(()=>{ resolve('success') },3000) }) p.then( res => { console.log('調用了onFullfilled') console.log(res) }, err => { console.log('調用了onRejected') console.log(err) } ) p.then( res => { console.log('調用了onFullfilled01') console.log(res) }, err => { console.log('調用了onRejected01') console.log(err) } ) // 3s后控制台輸出 調用了onFullfilled success 調用了onFullfilled01 success
- 但是目前then不支持鏈式調用,需要進行鏈式調用的支持
- 支持鏈式調用的條件
- 只有then函數調用返回一個promise對象,一個新的promise對象
newPromise
- then函數有一個返回值 這個返回值
result
就是新promise的onFulfill或onRejected的值
- 只有then函數調用返回一個promise對象,一個新的promise對象
- 接下來我們需要判斷這個返回值
result
的類型- 一個具體值
- 一個新的promise
- 另外我們需要一個函數去處理這種鏈式調用的問題
handlerChainPromise
,這個函數接收參數如下- result 老的promise的then返回值,作為新promise的onFulfill或onRejected
- resolve成功處理函數
- reject失敗處理函數
class Promise { then(onFulfilled, onRejected) { // 每一次調用返回一個promise return new Promise((resolve,reject) => { // 參數可選要進行判斷 如果傳遞不是一個函數 默認一個函數 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }; if (this.state === PENDING) { // 當resolve或者reject異步調用,then執行的時候promise狀態等待 // 將onFulfilled和onReject函數存儲起來 this.successCallBack.push(() => { let result = onFulfilled(this.value) this.handlerChainPromise(result, resolve, reject) }) this.errorCallBack.push(() => { let result = onRejected(this.reason) this.handlerChainPromise(result, resolve, reject) }) } if (this.state === FULFILLED) { // 當promise狀態為fulfilled時候調用onFulfilled let result = onFulfilled(this.value) this.handlerChainPromise(result, resolve, reject) } if (this.state === REJECTED) { // 當promise狀態為rejected時候調用onRejected let result = onRejected(this.reason) this.handlerChainPromise(result, resolve, reject) } }) } handlerChainPromise(result,resolve,reject){ // 如果返回的是一個promise就調用它的then方法 // 如果返回的是一個具體值就直接返回值 if(result instanceof Promise){ result.then(resolve, reject) }else{ resolve(result) } } }
- 測試代碼如下
// 返回一個具體值(可以使任何類型) const p = new Promise((resolve, reject) => { setTimeout(()=>{ resolve('success') },3000) }) p.then( res => { return 11 }, err => { console.log(err) } ).then( res=>{ console.log(res) } ) // 返回一個promsie const p = new Promise((resolve, reject) => { setTimeout(()=>{ resolve('success') },3000) }) p.then( res => { return new Promise(resolve=>{ setTimeout(()=>{ resolve(22) }, 1000) }) }, err => { console.log(err) } ).then( res=>{ console.log(res) } )
- 以上一個完整的promise就封裝成功了
- 完整代碼如下
// 狀態會多次用於判斷,所以建議用常量保存 const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class Promise { // 用來存儲狀態成功的回調函數 // 用來存儲狀態失敗的回調函數 successCallBack = [] errorCallBack = [] constructor(handler) { // 構造函數需要參數,參數必須是一個函數類型 這里面我們叫做handler,這兒會對handler類型進行驗證,如果不是函數類型會拋出類型異常 if (typeof handler !== 'function') { throw new TypeError(`Promise構造函數的參數${handler}不是一個函數`) } this.state = PENDING // 用來存儲promise的狀態 初始化狀態為pending this.reason = undefined // 拒因 this.value = undefined // 終值 // resolve,reject方法里面會用到實例的this,在調用的時候需要改變this的指向 handler(this.resolve.bind(this), this.reject.bind(this)) } resolve(value) { // 該方法只能在promise狀態為pending的時候調用,如果promise已經有了狀態,則不允許調用 if (this.state !== PENDING) return // 調用方法promise的狀態變為fulfilled this.state = FULFILLED // 調用該方法會得到一個終值value this.value = value // 異步調用的時候執行存儲的onFulfilled函數,傳入終值 this.successCallBack.forEach(fn => fn()) } reject(reason) { // 該方法只能在promise狀態為pending的時候調用,如果promise已經有了狀態,則不允許調用 if (this.state !== PENDING) return // 調用方法promise的狀態變為rejected this.state = REJECTED // 調用該方法會得到一個拒因reason this.reason = reason // 異步調用的時候執行存儲的onRejected函數,傳入拒因 this.errorCallBack.forEach(fn => fn()) } then(onFulfilled, onRejected) { // 每一次調用返回一個promise return new Promise((resolve,reject) => { // 參數可選要進行判斷 如果傳遞不是一個函數 默認一個函數 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }; if (this.state === PENDING) { // 當resolve或者reject異步調用,then執行的時候promise狀態等待 // 將onFulfilled和onReject函數存儲起來 this.successCallBack.push(() => { let result = onFulfilled(this.value) this.handlerChainPromise(result, resolve, reject) }) this.errorCallBack.push(() => { let result = onRejected(this.reason) this.handlerChainPromise(result, resolve, reject) }) } if (this.state === FULFILLED) { // 當promise狀態為fulfilled時候調用onFulfilled let result = onFulfilled(this.value) this.handlerChainPromise(result, resolve, reject) } if (this.state === REJECTED) { // 當promise狀態為rejected時候調用onRejected let result = onRejected(this.reason) this.handlerChainPromise(result, resolve, reject) } }) } handlerChainPromise(result,resolve,reject){ // 如果返回的是一個promise就調用它的then方法 // 如果返回的是一個具體值就直接返回值 if(result instanceof Promise){ result.then(resolve, reject) }else{ resolve(result) } } }
- onFulfilled