前端面試考點之---手寫Promise

離秋發表於2018-08-31

寫在前面:

在目前的前端分開中,我們對於非同步方法的使用越來越頻繁,那麼如果處理非同步方法的返回結果,如果優雅的進行非同步處理對於一個合格的前端開發者而言就顯得尤為重要,其中在面試中被問道最多的就是對Promise方法的掌握情況,本章將和大家一起分析和完成一個Promise方法,希望對你的學習有一定的幫助。

前端面試考點之---手寫Promise

瞭解Promise

既然我們是要模仿ES6的Promise,那我們必然要知道這個方法主要都是用來幹什麼的,有哪些引數,有什麼特性,為什麼要使用Promise及如何使用等等。

為什麼要使用它?

1.先統一執行AJAX邏輯,不關心如何處理結果,然後,在需要的時候處理AJAX結果

不知道大家有沒有思考過下面的問題,JavaScript的執行都是單執行緒的,但是如果我們要處理類似於網路請求(ajax),瀏覽器的一些事件等就要用到非同步執行,,大多都是下面這個樣子:

function callback() {
    console.log('我是一個回撥函式');
}
console.log('非同步方法之前');
setTimeout(callback, 1000); // 1秒鐘後呼叫callback函式
console.log('非同步方法之後');
複製程式碼

然後得到下面的結果:

前端面試考點之---手寫Promise

非同步操作會在將來的某個時間點觸發一個函式呼叫,AJAX就是典型的非同步操作。以jq程式碼為例:

$.ajax({
   type: "POST",
   url: "some.php",
   data: "name=John&location=Boston",
   success: function(msg){
     alert( "Data Saved: " + msg );
   }
});
複製程式碼

在上面的程式碼中我們雖然能夠得到ajax的操作結果,但是這種寫法不利於我們複用,說白了非同步的處理和返回結果在同一個塊內,很不美觀和優雅,下面來看看Promise是怎麼處理這樣的情況的:

let p = new Promise(function (resolve, reject) {
    setTimeout(() => {//使用定時器來模擬非同步
        resolve(100)
    }, 1000);
});
p.then(function (data) {
    console.log(data)
})
複製程式碼

可以看出p.then的呼叫可以是任何時候,只要我們需要時就可以拿到剛才返回結果。而不是像jq一樣在ajax有結果會就要對結果進行立即處理。

2.支援鏈式呼叫

在過去,我們要進行多重非同步請求的時候,一不小心就會形成回撥地獄,類似於下面的這樣:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);//三次函式巢狀呼叫之後得到結果
    }, failureCallback);
  }, failureCallback);
}, failureCallback);
複製程式碼

無疑,上面的函式在於閱讀性和維護性上面都讓我們有些力不從心,下面用Promise來實現一下上面的程式碼,就清晰的多:

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
複製程式碼

又細心的小夥伴會發現我們的錯誤處理都會被集中到catch中執行,這也就是我想說的第三個特點

3.通過捕獲所有的錯誤,promise解決了回撥厄運金字塔的基本缺陷。

說了這麼多,我想小夥伴已經多Promise有了一定的認識,那我就根據實際的使用,憑藉自己的理解和PromiseA+規範的描述,來實現一個屬於自己的promise

手寫符合規範的promise

先來看程式碼:

let p = new Promise((resolve,reject)=>{
  resolve();
  //reject();
})
複製程式碼

根據上面的程式碼我們可以看出,promise內部是一個立即執行的構造器函式,函式中有兩個引數分別為resolve,reject,所以我們自己的程式碼應該這樣寫

function Promise() {
    function resolve() { }
    function reject() { }
    executor(resolve,reject) 
 }
複製程式碼

可以看到我們得到了兩個函式resolve()和reject(),而且根據promiseA+規範文件中說明的:

前端面試考點之---手寫Promise
此處我們可以得到Promise有三個狀態 pending(等待狀態),fulfilled(成功狀態),rejected(失敗狀態);這個三個狀態之間的關係我們用一張圖來說明一下:

前端面試考點之---手寫Promise
首先Promise在執行的時候狀態都為pending,也就是等待狀態,然後等待狀態可以分別向成功狀態和失敗狀態轉換,但是一旦狀態不是pending狀態之後,這個promise的狀態就無法更改,且失敗狀態和成功狀態之間是不能相互轉換的,進一步完善程式碼如下:

前端面試考點之---手寫Promise

因為promise最強大的地方就在於then方法,所以不管是成功還是失敗我們最終都要將成功和失敗的值傳遞給then,為了方便呼叫,我們用兩個變數來接收各自的值

前端面試考點之---手寫Promise
上面已經提到promise最重要的方法就是then方法,那麼為了能夠在例項之後呼叫這個方法,我們必須將這個方法寫在他的原型鏈上面,並且他接受兩個引數,一個是成功的回撥,一個是失敗的回撥

前端面試考點之---手寫Promise

看下面的程式碼我們繼續分析接下來promise是進行怎麼操作的:

let p = new Promise((resolve,reject)=>{
  resolve(111);
})
p.then((value) => {
  console.log(value)
}, (reason) => {
  console.log('err', reason);
})
複製程式碼

上面的程式碼最終列印結果為111,這時候我們分析在promise中如果成功了,那麼then方法中的成功回撥就會立即執行,如果失敗了,失敗的回撥也會立即執行,所以我們可以繼續完善我們的程式碼:

前端面試考點之---手寫Promise
在上面的程式碼中我們只是使用同步方式,讓promise函式立即執行並傳入數字:111,如果是非同步的情況吶?讓我們進行下面的測試:

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(111);
    }, 1000);
  
})
p.then((value) => {
  console.log(value)
}, (reason) => {
  console.log('err', reason);
})
複製程式碼

這時候你會發現結果是在一秒鐘之後列印出來的,也就是說,then方法中成功和失敗的回撥,是在promise的非同步執行完成之後才被觸發的,所以你在呼叫then方法的時候promise的狀態一開始並不是成功或者失敗,而是先將成功和失敗的回撥函式儲存起來,等待非同步完成之後在執行相對應的成功或者失敗的回撥,所以接下來我們程式碼可以這樣寫:

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise
然後我們繼續進行嘗試,這次我們嘗試讓promise丟擲一個錯誤看它會怎麼處理?

前端面試考點之---手寫Promise
那麼反應在我們的程式碼中就可以這樣寫:

前端面試考點之---手寫Promise
在promise中我們可以進行鏈式呼叫的方式來多次的進行then,如同下面的程式碼:

let p = new Promise((resolve, reject) => {
    resolve(111)
  
})
p.then((value) => {
  return value+'第二次'
}, (reason) => {
  console.log('err', reason);
    }).then((data) => {
console.log(data)
    }, () => {
        
    })
複製程式碼

執行程式碼之後我們不難得到列印的結果為:111第二次,那麼也就是如果你的then方法的成功回撥函式如果返回一個值,那麼我們在下一個then方法中對應的成功回撥中也可以繼續使用這個值,換句話說,這個值會被當作下一次then中成功回撥的引數傳遞回來。 相同的我們測試如果出現錯誤的事情,會發現錯誤會傳遞給第二次的失敗中

let p = new Promise((resolve, reject) => {
    resolve(111)
  
})
p.then((value) => {
  throw new Error()
}, (reason) => {
  console.log('err', reason);
    }).then((data) => {
console.log(data)
    }, () => {
        console.log('第二得到失敗')
    })
複製程式碼

列印結果為:第二得到失敗 當然如果本次回撥函式中內容為空,那麼下次then中會直接走成功,而且如果是失敗之後也還是可以成功的,得到結果understand,如果你不想在then方法中處理錯誤,你還可以使用catch方法來最終捕獲錯誤,既然成功或者失敗中可以不寫引數,也就是這可以為一個空函式,也就是說then方法中的兩個引數都是可選引數:

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise
上面我們已經基本上嘗試了各種返回值,那麼還有一種情況也是我們需要考慮的,那就是如果返回一個promise方法會放生什麼情況?

p.then(() => { 
    return new Promise((resolve, reject) => { 
        resolve(111)
    })
}, (reason) => {
  
}).then((data) => {
    console.log('成功了',data)
}, (reason) => {
    
})
複製程式碼

列印結果為:成功了 111

經過嘗試如果返回的是一個promise函式,那麼他會等待這個promise執行完成之後在返回給下一次的then,promise如果成功,就會走下一次then的成功,如果失敗就會走下一次then的失敗。當然這裡需要注意的是,then方法中返回的回撥函式不能是自己本身,如果真的這樣寫,那麼函式執行到裡面時會等待promise的結果,這樣一層層的狀態等待就會形成回撥地獄

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise

前端面試考點之---手寫Promise
現在我們的程式碼已經看上去原生的promise很相似了,但是為了嚴謹,我們進行下面的嘗試:

let promise = new Promise((resolve,reject)=>{
   resolve();
});
promise.then((value) => { // pending
    return new Promise((resolve,reject)=>{
        return new Promise((resolve,reject)=>{
            resolve(111);
         })
     })
}, (reason) => {
  console.log(reason);
});
複製程式碼

理論上我們可以得出下一次then的結果為:111,因為我們是等待promise執行完才會返回,也就是說剛才我們的程式碼只是判斷了第一次是promise的情況,如果像上面程式碼的情況一樣,就會出現問題,為了規避這樣的問題,我們使用遞迴來執行:

前端面試考點之---手寫Promise

細心的你可能發現,上面的截圖中我還加入了一個called作為攔截器,那是因為如果有想我一樣的小白使用者,自己手寫的promise是既可以成功也可以失敗的,那麼這裡我們就要判斷一下,不能讓兩次呼叫都執行,只呼叫第一個被呼叫的

這樣我們的程式碼基本上就完美了,那我們就試一下吧:

let promise = new Promise((resolve,reject)=>{
   resolve(1);
});
promise.then((value) => { // pending
   console.log(value)
}, (reason) => {
  console.log(reason);
    });
console.log(2);
複製程式碼

你會發現我們的執行結果是1,2,但是在本文的最開始就已經提到promise是一個處理非同步的函式,執行結果應該為2,1才對,那是因為我們現在的promise的執行環境還是當前的上下文,也就是同步。做一下小小的改動,他就是非同步了:

前端面試考點之---手寫Promise
因為剛才分析得到then方法中兩個回撥函式可以是可選引數,所以我們也要處理一下:

前端面試考點之---手寫Promise

擴充套件方法實現

因為在我們的分析中還有一個catch方法,那我們也來實現一下吧。既然是可以鏈式呼叫的方法,那我們也必須寫在原型鏈上面:

Promise.prototype.catch = function (onrejected) {
  return this.then(null, onrejected)
}
複製程式碼

當然promise還可以直接使用resolve()和reject()直接呼叫,是一種簡便寫法:

Promise.reject = function (reason) {
  return new Promise((resolve, reject) => {
    reject(reason)
  })
}
Promise.resolve = function (value) {
  return new Promise((resolve, reject) => {
    resolve(value);
  })
}
複製程式碼

寫在最後

至此,我們所有的promise特性就已經一一實現了,你是否已經看明白了,當然作為一個小白選手,我還有很多的不足,歡迎大家的指正,你也可以去參考promiseA+規範中的文件去看看我寫的還有什麼需要補充的,歡迎交流。

PS:為什麼要結合promiseA+的規範?因為我們不能寫一個玩具程式碼來應付面試考官和自己,你需要讓自己的程式碼更具體有可讀性和實用性,需要去規避可能遇到的各種因為呼叫而產生的問題,讓你自己的程式碼更加無懈可擊,在使用場景上也會更加豐富

相關文章