很久沒有更新文章了,最近恰好遇到考試,並且一直在作數據庫課設。javascript
原本這篇文章是上個星期想要分享給工做室的師弟師妹們的,結果由於考試就落下了。html
其實我並非很想寫Promise
,畢竟如今更好的方式是結合await/async
和Promise
編寫異步代碼。可是,其實以爲Promise
這個東西對於入門ES6,改善「回調地獄」有很大的幫助,那也算是回過頭來複習一下吧。java
本文不少地方參考了阮一峯的《ES6標準入門》這一本書,由於學ES6,這本書是最好的,沒有之一。固然,整理的文章也有我本身的思路在,還有加上了本身的一些理解,適合入門ES6的小夥伴學習。node
若是已經對Promise
有必定的瞭解,但並無實際的用過,那麼能夠看一下在實例中使用和如何更加優雅的使用Promise
一節。mysql
另外,本文中有三個例子涉及「事件循環和任務隊列」(均已在代碼頭部標出),若是暫時不能理解,能夠先學完Promise
以後去了解最後一節的知識,而後再回來看,這樣小夥伴你應該就豁然開朗了。jquery
所謂回調,就是「回來調用」,這裏拿知乎上「常溪玲」一個很形象的例子: 「 你到一個商店買東西,恰好你要的東西沒有貨,因而你在店員那裏留下了你的電話,過了幾天店裏有貨了,店員就打了你的電話,而後你接到電話後就到店裏去取了貨。在這個例子裏,你的電話號碼就叫回調函數,你把電話留給店員就叫登記回調函數,店裏後來有貨了叫作觸發了回調關聯的事件,店員給你打電話叫作調用回調函數,你到店裏去取貨叫作響應回調事件。」git
至於回調函數的官方定義是什麼,這裏就不展開了,畢竟和咱們本篇文章關係不大。有興趣的小夥伴能夠去知乎搜一下。es6
寫過node
代碼的小夥伴必定會遇到這樣的一個調用方式,好比下面mysql
數據庫的查詢語句:github
connection.query(sql1, (err, result) => { //ES6箭頭函數 //第一次查詢 if(err) { console.err(err); } else { connection.query(sql2, (err, result) => { //第二次查詢 if(err) { console.err(err); } else { ... } }; } })
上面的代碼大概的意思是,使用mysql
數據庫進行查詢數據,當執行完sql1
語句以後,再執行sql2
語句。web
可見,上面執行sql1
語句和sql2
語句有一個前後的過程。爲了實現先去執行sql1
語句再執行sql2
語句,咱們只能這樣簡單粗暴的去嵌套調用。
若是隻有兩三步操做還好,那麼假如是十步操做或者更多,那代碼的結構是否是更加的複雜了並且還難以閱讀。
因此,Promise
就爲了解決這個問題,而出現了。
這一部分的內容絕大部分摘抄自《ES6標準入門》一書,若是你已經讀過相關Promise
的使用方法,那麼你大能夠快速瀏覽或直接跳過。
同時,你更須要留意一下catch
部分和涉及「事件循環」的三個例子。
所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件的結果。從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。Promise 提供統一的 API,各類異步操做均可以用一樣的方法進行處理,讓開發者不用再關注於時序和底層的結果。Promise的狀態具備不受外界影響和不可逆兩個特色,與譯後的「承諾」這個詞有着類似的特色。
首先,Promise
對象表明一個異步操做,有三種狀態:pending
(進行中)、fulfilled
(已成功)、rejected
(已失敗)。
只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒有辦法改變這個狀態。
其次,狀態是不可逆的。也就是說,一旦狀態改變,就不會再變成其餘的了,日後不管什麼時候,均可以獲得這個結果。
對於Promise
的狀態的改變,只有兩種狀況:一是pending
變成fulfilled
,一是pending
變成rejected
。(注:下文用resolved
指代fulfilled
)
只要這兩種狀況中的一種發生了,那麼狀態就被固定下來了,不會再發生改變。
同時,若是改變已經發生了,此時再對Promise
對象指定回調函數,那麼會當即執行添加的回調函數,返回Promise
的狀態。這與事件徹底不一樣。事件的狀態是瞬時性的,一旦錯過,它的狀態將不會被保存。此時再去監聽,確定是得不到結果的。
ES6規定,Promise對象是一個構造函數,用來生成Promise實例。
實例對象
這裏,咱們先來new
一個全新的Promise
實例。
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 異步操做成功*/) { resolve(value); } else { reject(error); } });
能夠看到,Promise
構造函數接受一個匿名函數做爲參數,在函數中,又分別接受resolve
和reject
兩個參數。這兩個參數表明着內置的兩個函數。
resovle
的做用是,將Promise
對象的狀態從「未完成(pending
)」變爲「成功(resolved
)」,一般在異步操做成功時調用,並將異步操做的結果,作爲它的參數傳遞出去。
reject
的做用是,將Promise
對象的狀態從「未完成(pending
)」變成"失敗(rejected
)",一般在異步操做失敗時調用,並將異步操做的結果,做爲參數傳遞出去。
接收狀態的回調
在Promise
實例生成之後,可使用then
方法指定resolved
狀態和rejected
狀態。
//接上「實例對象」的代碼 promise.then(function(value) { //success },function(error) { //failure });
可見,then
方法能夠接受兩個回調函數做爲參數。第一個回調函數是Promise
對象的狀態變爲resolved
時調用,第二個回調函數是promise
對象的狀態變爲rejected
時調用。其中,第二個函數是可選的。並不必定要提供。另外,這兩個函數都接受Promise
對象傳出的值做爲參數。
下面給出了一個簡單的例子:
function timeout(ms) { return new Promise((resolve, reject) { setTimeout(resolve, ms, 'done'); }); } timeout(100).then(function(value) { console.log(value); //done });
上面的例子,是在100ms以後,把新建的Promise
對象由pending
狀態變爲resolved
狀態,接着觸發then
方法綁定的回調函數。
另外,Promise
在新建的時候就會當即執行,所以咱們也能夠直接改變Promise
的狀態。
//涉及「事件循環」例子1 let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved
上面的代碼中,新建了一個Promise
實例後當即執行,因此首先輸出的是"Promise"
,僅接着resolve
以後,觸發then
的回調函數,它將在當前腳本全部同步任務執行完了以後纔會執行,因此接下來輸出的是"Hi!"
,最後纔是"resolved"
。(注:這裏涉及到JS的任務執行過程和事件循環,若是還不是很瞭解這個流程能夠所有看完後再回過來理解一下這段代碼。)
關於Promise
的基本用法,就先講解到這裏。
接下來咱們來看一下Promise
封裝的原生方法。
then
和catch
Promise.prototype.then
Promise
的原型上有then
方法,前面已經說起和體驗過,它的做用是爲Promise
實例添加狀態改變時的回調函數。 then
的方法的第一個參數是resolved
狀態的回調函數,第二個參數(可選)是rejected
狀態的回調函數。
then
方法返回的是一個新的Promise
實例,所以能夠採用鏈式寫法,也就是說在then
後面能夠再調用另外一個then
方法。
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 異步操做成功*/) { resolve(obj); } else { reject(error); } }); promise.then(function(obj) { return obj.a; }).then(function(a) { //... });
上面的代碼使用then
方法,依次指定了兩個回調函數。第一個回調函數完成之後,會將返回結果做爲參數,傳入第二個回調函數。
也就是說,在Promise
中傳參有兩種方式:
一是實例Promise
的時候把參數經過resovle()
傳遞出去。
二是在then
方法中經過return
返回給後面的then
。
採用鏈式的then
,能夠指定一組按照次序調用的回調函數。這時,前一個回調函數,有可能返回的仍是一個Promise
對象(即有異步操做),這時後一個回調函數,就會等待該Promise
對象的狀態發生變化,纔會被調用。
const promise1 = new Promise(function(resolve, reject) { // ... some code if(/* 異步操做成功*/) { resolve("promise1"); } else { reject(error); } }); const promise2 = new Promise(function(resolve, reject) { // ... some code if(/* 異步操做成功*/) { resolve("promise2"); } else { reject(error); } }); promise1.then(function() { return promise2; }).then(function funcA(result) { console.log(result); //"promise2" }, function funcB(err){ console.log("rejected: ", err); });
上面代碼中,第一個then
方法指定的回調函數,返回的是另外一個Promise
對象。這時,第二個then
方法指定的回調函數,就會等待這個新的Promise
對象狀態發生變化。若是變爲resolved
,就調用funcA
,若是狀態變爲rejected
,就調用funcB
。
Promise.prototype.catch
Promise.prototype.catch
方法是.then(null, rejection)
的別名,用於指定發生錯誤時的回調函數。
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 異步操做成功*/) { resolve(value); } else { reject(error); } }); promise.then(function(value) { //success },function(error) { //failure });
因而,這段代碼等價爲:
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 異步操做成功*/) { resolve(value); } else { reject(error); } }) promise.then(function() { //success }).catch(function(err) { //failure })
可見,此時「位置1」中的then
裏面的兩個參數被剝離開來,若是異步操做拋出錯誤,就會調用catch
方法指定的回調函數,處理這個錯誤。
值得一提的是,如今咱們在給rejected
狀態綁定回調的時候,更傾向於catch
的寫法,而不使用then
方法的第二個參數。這種寫法,不只讓Promise
看起來更加簡潔,更加符合語義邏輯,接近try/catch
的寫法。更重要的是,Promise
對象的錯誤具備向後傳遞的性質(書中說「冒泡」我以爲不是很合適,可能會誤解),直到錯誤被捕獲爲止。
const promise1 = new Promise(function(resolve, reject) { // ... some code if(/* 異步操做成功*/) { resolve("promise1"); } else { reject(error); } }); const promise2 = new Promise(function(resolve, reject) { // ... some code if(/* 異步操做成功*/) { resolve("promise2"); } else { reject(error); } }); promise1.then(function() { return promise2; }).then(function funcA(result) { console.log(result); //"promise2" }).catch(function(err) { console.log(err); //處理錯誤 })
上面的代碼中一共有三個Promise
,第一個由promise1
產生,另外兩個由不一樣的兩個then
產生。不管是其中的任何一個拋出錯誤,都會被最後一個catch
捕獲。
若是仍是對Promise
錯誤向後傳遞的性質不清楚,那麼能夠按照下面的代碼作一下實驗,即可以更加清晰的認知這個特性。
const promise1 = new Promise(function(resolve, reject) { //1. 在這裏throw("promise1錯誤"),catch捕獲成功 // ... some code if(true) { resolve("promise1"); } else { reject(error); } }); const promise2 = new Promise(function(resolve, reject) { // ... some code //2. 在這裏throw("promise2錯誤"),catch捕獲成功 if(true) { resolve("promise2"); } else { reject(error); } }); promise1.then(function() { return promise2; }).then(function funcA(result) { console.log(result); //"promise2" //3. 在這裏throw("promise3錯誤"),catch捕獲成功 }).catch(function(err) { console.log(err); //處理錯誤 })
以上,分別將一、二、3的位置進行解註釋,就可以證實咱們以上的結論。
關於catch
方法,還有三點須要說起的地方。
Promise
中的錯誤傳遞是向後傳遞,並不是是嵌套傳遞,也就是說,嵌套的Promise
,外層的catch
語句是捕獲不到錯誤的。
const promise1 = new Promise(function(resolve, reject) { // ... some code if(true) { resolve("promise1"); } else { reject(error); } }); const promise2 = new Promise(function(resolve, reject) { // ... some code if(true) { resolve("promise2"); } else { reject(error); } }); promise1.then(function() { promise2.then(function() { throw("promise2出錯"); }) }).catch(function(err) { console.log(err); }); //> Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined} //Uncaught (in promise) promise2出錯
因此,代碼出現了未捕獲的錯誤,這就是爲何我強調說是「向後傳遞錯誤而不是冒泡傳遞錯誤」。
在Promise
沒有使用catch
而拋出未處理的錯誤。
const someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行會報錯,由於x沒有聲明 resolve(x + 2); }); }; someAsyncThing().then(function() { console.log('everything is great'); }); setTimeout(() => { console.log(123) }, 2000); // Uncaught (in promise) ReferenceError: x is not defined // 123
上面代碼中,someAsyncThing
函數產生的Promise
對象,內部有語法錯誤。瀏覽器運行到這一行,會打印出錯誤提示ReferenceError: x is not defined
,可是不會退出進程、終止腳本執行,2秒以後仍是會輸出123
。這就是說,Promise
內部的錯誤不會影響到Promise
外部的代碼,通俗的說法就是「Promise
會吃掉錯誤」。
解決的方法就是在then
後面接一個catch
方法。
涉及到Promise
中的異步任務拋出錯誤的時候。
//涉及「事件循環」例子2 const promise = new Promise(function (resolve, reject){ resolve('ok'); setTimeout(function () { throw new Error('test') }, 0); }); promise.then(function (value) { console.log(value); }).catch(function(err) { console.log(err); }); // ok // Uncaught Error: test
能夠看到,這裏的錯誤並不會catch
捕獲,結果就成了一個未捕獲的錯誤。
緣由有二:
其一,因爲在setTimeout
以前已經resolve
過了,因爲這個時候的Promise
狀態就變成了resolved
,因此它走的應該是then
而不是catch
,就算後面再拋出錯誤,因爲其狀態不可逆的緣由,依舊不會拋出錯誤。也就是下面這種狀況:
const promise = new Promise(function (resolve, reject) { resolve('ok'); throw new Error('test'); //依然不會拋出錯誤 }); //...省略
其二,setTimeout
是一個異步任務,它是在下一個「事件循環」才執行的。當到了下一個事件循環,此時Promise
早已經執行完畢了,此時這個錯誤並非在Promise
內部拋出了,而是在全局做用域中,因而成了未捕獲的錯誤。(注:這裏涉及到JS的任務執行過程和事件循環,若是還不是很瞭解這個流程能夠所有看完後再回過來理解一下這段代碼。)
解決的方法就是直接在setTimeout
的回調函數中去try/catch
。
Promise.resolve
這個方法能夠把現有的對象轉換成一個Promise
對象,以下:
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代碼把jQuery中生成的deferred
對象轉換成了一個新的Promise
對象。
Promise
的參數大體分下面四種:
若是參數是Promise
實例,那麼Promise.resolve
將不作任何修改、原封不動地返回這個實例。
參數是一個thenable
對象。
thenable
對象指的是具備then
方法的對象,好比下面這個對象。
let thenable = { then: function(resolve, reject) { resolve(42); } };
Promise.resolve
方法會將這個對象轉爲Promise
對象,而後就當即執行thenable
對象的then
方法,以下:
let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // 42 });
參數不是具備then方法的對象,或根本就不是對象。
若是參數是一個原始值,或者是一個不具備then
方法的對象,則Promise.resolve
方法返回一個新的Promise
對象,狀態爲resolved
。
不帶有任何參數。
Promise.resolve
方法容許調用時不帶參數,直接返回一個resolved
狀態的Promise
對象。
//涉及「事件循環」例子3 setTimeout(function () { console.log('three'); }, 0); Promise.resolve().then(function () { console.log('two'); }); console.log('one'); // one // two // three
上面這個例子,因爲Promise
算是一個微任務,當第一次事件循環執行完了以後(console.log('one')
),會取出任務隊列中的全部微任務執行完(Promise.resovle().then
),再進行下一次事件循環,也就是以後再執行setTimeout
。因此輸出的順序就是one
、two
、three
。(注:這裏涉及到JS的任務執行過程和事件循環,若是還不是很瞭解這個流程能夠所有看完後再回過來理解一下這段代碼。)
Promise.reject
Promise.reject(reason)
方法也會返回一個新的Promise
實例,該實例的狀態爲rejected
,並當即執行其回調函數。
注意,Promise.reject()
方法的參數,會原封不動地做爲reject
的理由,變成後續方法的參數。這一點與Promise.resolve
方法不一致。
const thenable = { then(resolve, reject) { reject('出錯了'); } }; Promise.reject(thenable) .catch(e => { console.log(e === thenable) }); // true
上面代碼中,Promise.reject
方法的參數是一個thenable
對象,執行之後,後面catch
方法的參數不是reject
拋出的「出錯了」這個字符串,而是thenable
對象。
下面的方法只作簡單的介紹,若是須要更詳細的瞭解它,請到傳送門處查詢相關資料。
Promise.all
Promise.all
方法用於將多個Promise
實例,包裝成一個新的Promise
實例。
const p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.all
方法接受一個數組做爲參數,p1
、p2
、p3
都是Promise
實例,若是不是,就會先調用上面講到的Promise.resolve
方法,將參數轉爲Promise
實例,再進一步處理。
p
的狀態由p1
、p2
、p3
決定,分紅兩種狀況。
(1)只有p1
、p2
、p3
的狀態都變成fulfilled
,p
的狀態纔會變成fulfilled
,此時p1
、p2
、p3
的返回值組成一個數組,傳遞給p
的回調函數。
(2)只要p1
、p2
、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的實例的返回值,會傳遞給p
的回調函數。
Promise.race
const p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.race
方法接受一個數組做爲參數,p1
、p2
、p3
都是Promise
實例,若是不是,就會先調用上面講到的Promise.resolve
方法,將參數轉爲Promise
實例,再進一步處理。
與Promise.all
不一樣,只要其中有一個實例率先改變狀態,p
的狀態就跟着改變。那麼率先改變的Promise
實例的返回值,就傳遞給p
的回調函數。
done
Promise
對象的回調鏈,無論以then
方法或catch
方法結尾,要是最後一個方法拋出錯誤,都有可能沒法捕捉到。所以,咱們能夠提供一個done
方法,老是處於回調鏈的尾端,保證拋出任何可能出現的錯誤。
它的實現代碼至關簡單。
Promise.prototype.done = function (onFulfilled, onRejected) { this.then(onFulfilled, onRejected) .catch(function (reason) { // 拋出一個全局錯誤 setTimeout( function() { throw reason }, 0); }); };
從上面代碼可見,done
方法的使用,能夠像then
方法那樣用,提供fulfilled
和rejected
狀態的回調函數,也能夠不提供任何參數。但無論怎樣,done
都會捕捉到任何可能出現的錯誤,並向全局拋出。
finally
finally
方法用於指定無論Promise
對象最後狀態如何,都會執行的操做。它與done
方法的最大區別,它接受一個普通的回調函數做爲參數,該函數無論怎樣都必須執行。
下面是一個例子,服務器使用Promise
處理請求,而後使用finally
方法關掉服務器。
server.listen(0) .then(function () { // run test }); .finally(server.stop);
它的實現也很是的簡單。
Promise.prototype.finally = function (callback) { let P = this.constructor; return this.then( function(value) { P.resolve(callback()).then(function() { return value; }); }, function(reason) { reason => P.resolve(callback()).then(function() { throw reason; }); }); };
最初,在低版本的JQuery中,對於回調函數,它的功能是很是弱的。無限「嵌套」回調,編程起來十分不友好。爲了改變這個問題,JQuery團隊就設計了deferred
對象。
它把回調的嵌套調用改寫成了鏈式調用,具體的寫法也十分的簡單。這裏也不詳細講,想了解的小夥伴也能夠直接到這個連接去看。傳送門
可是,因爲deferred
對象它的狀態能夠在外部被修改到,這樣會致使混亂的出現,因而就有了deferred.promise
。
它是在原來的deferred
對象上返回另一個deferred
對象,後者只開放與改變執行狀態無關的方法,屏蔽與改變執行狀態有關的方法。從而來避免上述提到的外部修改狀態的狀況。
若是有任何疑問,能夠回到傳送門一看便知。
值得一提的是,JQuery中的Promise
與咱們文章講的Promise
並無關係,只是名字同樣罷了。
雖然二者遵循的規範不相同,可是都致力於一件事情,那就是:基於回調函數更好的編程方式。
既然咱們學了Promise
,那麼就應該在平常開發中去使用它。
然而,對於初學者來講,在使用Promise
的時候,可能會出現嵌套問題。
好比說下面的代碼:
var p1 = new Promise(function() { if(...) { reject(...); } else { resolve(...); } }); var p2 = new Promise(function() { if(...) { reject(...); } else { resolve(...); } }); var p3 = new Promise(function() { if(...) { reject(...); } else { resolve(...); } }); p1.then(function(p1_data) { p2.then(function(p2_data) { // do something with p1_data p3.then(fuction(p3_data) { // do something with p2_data // p4... }); }); });
假如說如今須要p1
、p2
、p3
按照順序執行,那麼剛入門的小夥伴可能會這樣寫。
其實也沒有錯,這裏是用了Promise
,可是用得並不完全,依然存在「回調」地獄,沒有深刻到Promise
的核心部分。
那麼咱們應該怎麼樣更好的去運用它呢?
回顧一下前面Promise
部分,你應該能夠獲得答案。
下面,看咱們修正後的代碼。
//同上,省略定義。 p1.then(function(p1_data) { return p2; //位置1 }).then(function(p2_data){ //位置2 return p3; }).then(function(p3_data){ return p4; }).then(function(p4_data){ //final result }).catch(function(error){ //同一處理錯誤信息 });
能夠看到,每次執行完了then
方法以後,咱們都return
了一個新的Promise
。那麼當新的Promise
中resolve
以後,那麼顯而易見的,它會執行跟在它後面的then
之中。
也就是說,在p1
的then
方法執行完了以後,如今咱們要去執行p2
,那麼這個時候咱們在「位置1」給它return
了一個新的Promise
,因此此時的代碼能夠等價爲:
p2.then(function(p2_data){ //位置2 return p3; }).then(function(p3_data){ return p4; }).then(function(p4_data){ //final result }).catch(function(error){ //同一處理錯誤信息 });
可見,p2
中resolve
以後,就能夠被「位置2」的then
接收到了。
因而,基於這個結構,咱們就能夠在開發中去封裝出一個Promise
供咱們來使用。
恰好最近在作一個mysql
的數據庫課設,這裏就把我如何封裝promise
給貼出來。
下面的例子,可能有些接口剛接觸node
的小夥伴會看不懂,那麼,我會盡可能的作到無死角註釋,你們也儘可能關注一下封裝的過程(注:重點關注標「*」的地方)。
首先是mysql.js
封裝文件。
var mysql = require("mysql");//引入mysql庫 //建立一個鏈接池,同一個鏈接池能夠同時存在多個鏈接,鏈接完成須要釋放 var pool = mysql.createPool({ ...//省略鏈接的配置 }); /** * 把mySQL查詢功能封裝成一個promise * @param String sql * @returns Promise **/ var QUERY = (sql) => { //注意這裏new了一個新的promise(*) var connect = new Promise((resolve, reject) => { //建立鏈接 pool.getConnection((err, connection) => { //下面是狀態執行(*) if (err) { reject(err);//若是建立鏈接失敗的話直接reject(*) } else { //不然能夠進行查詢了 connection.query(sql, (err, results) => { //執行完查詢釋放鏈接 connection.release(); //在查詢的時候若是出錯直接reject if (err) { reject(err);//(*) } else { //不然成功,把查詢的結果resolve出去 //而後給後面then去使用 resolve(results);//(*) } }); } }); }); //最後把promise給return出去(*) return connect; }; module.exports = QUERY; //把封裝好的庫導出
接下來,去使用咱們封裝好的查詢Promise
。
假如咱們如今想要使用查詢功能獲取某個數據表的全部數據:
var QUERY = require("mysql"); //把咱們寫的庫給導入 var sql = `SELECT * FROM student`;//sql語句,看不懂直接忽略 //執行查詢操做 QUERY(sql).then((results) => { //(*) //這裏就可使用查詢到的results了 }).catch((err) => { //使用catch能夠捕獲到整條鏈拋出的錯誤。(*) console.log(err); })
以上,就是一個實例了。因此之後,若是你想要封裝一個Promise
來使用,你能夠這樣來寫。
那麼,如今問題又來了,若是咱們如今須要進行不少異步操做(好比Ajax通訊),那麼若是按照上面的寫法,會致使then
鏈條過長。因而,須要咱們不停的去return
一個新的Promise
對象供後面使用。以下:
function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse); }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse); } }; function main() { function recordValue(results, value) { results.push(value); return results; } // [] 用來保存初始化的值 至關於聲明results = [] var pushValue = recordValue.bind(null, []); return request.comment() //位置1 .then(pushValue) .then(request.people) .then(pushValue); } // 運行示例 main().then(function (value) { console.log(value); }).catch(function(error){ console.error(error); });
能夠看到,在「位置1」處的代碼,return request.comment().then(pushValue).then(request.people).then(pushValue);
使用了三個then
和new
了兩個新的Promise
。
所以,若是咱們將處理內容統一放到數組裏,再配合for
循環進行處理的話,那麼處理內容的增長將不會再帶來什麼問題。首先咱們就使用for
循環來完成和前面一樣的處理。
function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse); }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse); } };
前面這一部分是不須要改變的。
function main() { function recordValue(results, value) { results.push(value); return results; } // [] 用來保存初始化值 var pushValue = recordValue.bind(null, []); // 返回promise對象的函數的數組 var tasks = [request.comment, request.people]; var promise = Promise.resolve(); // 開始的地方 for (var i = 0; i < tasks.length; i++) { var task = tasks[i]; promise = promise.then(task).then(pushValue); } return promise; } // 運行示例 main().then(function (value) { console.log(value); }).catch(function(error){ console.error(error); });
使用for
循環的時候,每次調用then
都會返回一個新建立的Promise
對象 所以相似promise = promise.then(task).then(pushValue);
的代碼就是經過不斷對promise
進行處理,不斷的覆蓋 promise
變量的值,以達到對Promise
對象的累積處理效果。 可是這種方法須要promise
這個臨時變量,從代碼質量上來講顯得不那麼簡潔。 若是將這種循環寫法改用Array.prototype.reduce
的話,那麼代碼就會變得聰明多了。
因而咱們再對main
函數進行修改:
function main() { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); var tasks = [request.comment, request.people]; return tasks.reduce(function (promise, task) { return promise.then(task).then(pushValue); }, Promise.resolve()); }
(注:Array.prototype.reduce
第一個參數執行數組每一個值的回調函數,第二個參數是初始值。回調函數中,第一個參數是上一次調用回調返回的值或提供的初始值,第二個是數組中正在處理的元素。)
最後,重寫完了整個函數就是:
function sequenceTasks(tasks) { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); return tasks.reduce(function (promise, task) { return promise.then(task).then(pushValue); }, Promise.resolve()); } function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); }); } var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse); }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse); } }; function main() { return sequenceTasks([request.comment, request.people]); } // 運行示例 main().then(function (value) { console.log(value); }).catch(function(error){ console.error(error); });
須要注意的是,在sequenceTasks
中傳入的應該是返回Promise
對象的函數的數組,而不是一個Promise
對象,由於一旦返回一個對象的時候,異步任務其實已是開始執行了。
綜上,在寫順序隊列的時候,核心思想就是不斷的去return
新的Promise
並進行狀態判斷 。而至於怎麼寫,要根據實際狀況進行編程。
本質上來講,回調自己沒有什麼很差的,可是由於回調的存在,使得咱們無限的嵌套函數構成了「回調地獄」,這對開發者來講無疑是特別不友好的。而雖然Promise
只是回調的語法糖,可是卻提供給咱們更好的書寫方式,解決了回調地獄嵌套的難題。
最後,這裏算是一個拓展和學習方向,學習起來有必定的難度。
因爲JavaScript
是一種單線程的語言,所謂的單線程就是按照咱們書寫的代碼同樣一行一行的執行下來,因而每次只能作一件事。
若是咱們不是用異步的方式而用同步的方式去處理任務,假如如今咱們有一個網絡請求,請求後面是與其無關的一些操做代碼。那麼當請求發送出去的時候,因爲如今執行代碼是循序漸進的,因而咱們就必須等待網絡請求的應答以後,咱們才能繼續往下執行咱們的代碼。而這個等待,不只花費了咱們不少時間。同時,也阻塞了咱們後面的代碼。形成了沒必要要的資源浪費。
因而,當使用異步的方式來處理任務的時候,每次發送請求,JavaScript
中的執行棧會把異步操做交給瀏覽器的webCore
內核來處理,而後繼續往下執行代碼。當主線程的執行棧代碼執行完畢以後,就會去檢查任務隊列中有沒有任務須要執行的。
若是有,則取出來到主線程的執行棧中執行,執行完畢後,更新dom
,而後再進行一次一樣的循環。
而任務隊列中任務的添加,則是靠瀏覽器內核webCore
。每次異步操做完成以後,webCore
內核就會把相應的回調函數添加到任務隊列中。
值得注意的是,任務隊列中任務按照任務性質劃分爲宏任務和微任務。而因爲任務類型的不一樣,可能存在多個類型的任務隊列。可是事件循環只能有一個。
因此如今咱們把宏任務和微任務考慮進去,第一次執行完腳本的代碼(算是一次宏任務),那麼就會到任務隊列的微任務隊列中取出其全部任務放到主線程的執行棧中來執行,執行完畢後,更新dom
。下一次事件循環,再從任務隊列中取出一個宏任務,而後執行微任務隊列中的全部微任務。再循環...
注:第一次執行代碼的時候,就已經開始了第一次的事件循環,此時的script
同步代碼是一個宏任務。
整個過程,也就是下面的這一個圖:
常見的異步任務有:網絡請求、IO操做、計時器和事件綁定等。
以上,若是你可以看懂我在講什麼,那麼說明你真正理解了JS中的異步,若是不懂,那麼你須要去了解一下「事件循環、任務隊列、宏任務與微任務」,下面是兩篇不錯的博客,值得學習。
事件循環:http://www.ruanyifeng.com/blo...
對JS異步任務執行的一個總結:http://www.yangzicong.com/art...