寫在前面:php
在目前的前端分開中,咱們對於異步方法的使用愈來愈頻繁,那麼若是處理異步方法的返回結果,若是優雅的進行異步處理對於一個合格的前端開發者而言就顯得尤其重要,其中在面試中被問道最多的就是對Promise方法的掌握狀況,本章將和你們一塊兒分析和完成一個Promise方法,但願對你的學習有必定的幫助。前端
既然咱們是要模仿ES6的Promise,那咱們必然要知道這個方法主要都是用來幹什麼的,有哪些參數,有什麼特性,爲何要使用Promise及如何使用等等。面試
不知道你們有沒有思考過下面的問題,JavaScript的運行都是單線程的,可是若是咱們要處理相似於網絡請求(ajax),瀏覽器的一些事件等就要用到異步執行,,大多都是下面這個樣子: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是怎麼處理這樣的狀況的:bash
let p = new Promise(function (resolve, reject) {
setTimeout(() => {//使用定時器來模擬異步
resolve(100)
}, 1000);
});
p.then(function (data) {
console.log(data)
})
複製代碼
能夠看出p.then的調用能夠是任什麼時候候,只要咱們須要時就能夠拿到剛纔返回結果。而不是像jq同樣在ajax有結果會就要對結果進行當即處理。網絡
在過去,咱們要進行多重異步請求的時候,一不當心就會造成回調地獄,相似於下面的這樣:異步
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中執行,這也就是我想說的第三個特色
說了這麼多,我想小夥伴已經多Promise有了必定的認識,那我就根據實際的使用,憑藉本身的理解和PromiseA+規範的描述,來實現一個屬於本身的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有三個狀態 pending(等待狀態),fulfilled(成功狀態),rejected(失敗狀態);這個三個狀態之間的關係咱們用一張圖來講明一下: 首先Promise在執行的時候狀態都爲pending,也就是等待狀態,而後等待狀態能夠分別向成功狀態和失敗狀態轉換,可是一旦狀態不是pending狀態以後,這個promise的狀態就沒法更改,且失敗狀態和成功狀態之間是不能相互轉換的,進一步完善代碼以下:由於promise最強大的地方就在於then方法,因此不論是成功仍是失敗咱們最終都要將成功和失敗的值傳遞給then,爲了方便調用,咱們用兩個變量來接收各自的值
上面已經提到promise最重要的方法就是then方法,那麼爲了可以在實例以後調用這個方法,咱們必須將這個方法寫在他的原型鏈上面,而且他接受兩個參數,一個是成功的回調,一個是失敗的回調看下面的代碼咱們繼續分析接下來promise是進行怎麼操做的:
let p = new Promise((resolve,reject)=>{
resolve(111);
})
p.then((value) => {
console.log(value)
}, (reason) => {
console.log('err', reason);
})
複製代碼
上面的代碼最終打印結果爲111,這時候咱們分析在promise中若是成功了,那麼then方法中的成功回調就會當即執行,若是失敗了,失敗的回調也會當即執行,因此咱們能夠繼續完善咱們的代碼:
在上面的代碼中咱們只是使用同步方式,讓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中咱們能夠進行鏈式調用的方式來屢次的進行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('第二獲得失敗')
})
複製代碼
上面咱們已經基本上嘗試了各類返回值,那麼還有一種狀況也是咱們須要考慮的,那就是若是返回一個promise方法會放生什麼狀況?打印結果爲:第二獲得失敗 固然若是本次回調函數中內容爲空,那麼下次then中會直接走成功,並且若是是失敗以後也仍是能夠成功的,獲得結果understand,若是你不想在then方法中處理錯誤,你還可使用catch方法來最終捕獲錯誤,既然成功或者失敗中能夠不寫參數,也就是這能夠爲一個空函數,也就是說then方法中的兩個參數都是可選參數:
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很類似了,可是爲了嚴謹,咱們進行下面的嘗試: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的狀況,若是像上面代碼的狀況同樣,就會出現問題,爲了規避這樣的問題,咱們使用遞歸來執行:
細心的你可能發現,上面的截圖中我還加入了一個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的執行環境仍是當前的上下文,也就是同步。作一下小小的改動,他就是異步了:
由於剛纔分析獲得then方法中兩個回調函數能夠是可選參數,因此咱們也要處理一下:由於在咱們的分析中還有一個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+的規範?由於咱們不能寫一個玩具代碼來應付面試考官和本身,你須要讓本身的代碼更具體有可讀性和實用性,須要去規避可能遇到的各類由於調用而產生的問題,讓你本身的代碼更加無懈可擊,在使用場景上也會更加豐富