摘要:采用鏈?zhǔn)降?,可以指定一組按照次序調(diào)用的回調(diào)函數(shù)。異步操作成功異步操作成功上面代碼中,第一個方法指定的回調(diào)函數(shù),返回的是另一個對象。這時,第二個方法指定的回調(diào)函數(shù),就會等待這個新的對象狀態(tài)發(fā)生變化。方法是的別名,用于指定發(fā)生錯誤時的回調(diào)函數(shù)。
好久沒有更新文章了,最近剛好遇到考試,而且一直在做數(shù)據(jù)庫課設(shè)。
本來這篇文章是上個星期想要分享給工作室的師弟師妹們的,結(jié)果因?yàn)榭荚嚲吐湎铝恕?/p>
其實(shí)我并不是很想寫Promise,畢竟現(xiàn)在更好的方式是結(jié)合await/async和Promise編寫異步代碼。但是,其實(shí)覺得Promise這個東西對于入門ES6,改善“回調(diào)地獄”有很大的幫助,那也算是回過頭來復(fù)習(xí)一下吧。
本文很多地方參考了阮一峰的《ES6標(biāo)準(zhǔn)入門》這一本書,因?yàn)閷W(xué)ES6,這本書是最好的,沒有之一。當(dāng)然,整理的文章也有我自己的思路在,還有加上了自己的一些理解,適合入門ES6的小伙伴學(xué)習(xí)。
如果已經(jīng)對Promise有一定的了解,但并沒有實(shí)際的用過,那么可以看一下在實(shí)例中使用和如何更加優(yōu)雅的使用Promise一節(jié)。
另外,本文中有三個例子涉及“事件循環(huán)和任務(wù)隊(duì)列”(均已在代碼頭部標(biāo)出),如果暫時不能理解,可以先學(xué)完Promise之后去了解最后一節(jié)的知識,然后再回來看,這樣小伙伴你應(yīng)該就豁然開朗了。
引言 回調(diào)函數(shù)所謂回調(diào),就是“回來調(diào)用”,這里拿知乎上“常溪玲”一個很形象的例子: “ 你到一個商店買東西,剛好你要的東西沒有貨,于是你在店員那里留下了你的電話,過了幾天店里有貨了,店員就打了你的電話,然后你接到電話后就到店里去取了貨。在這個例子里,你的電話號碼就叫回調(diào)函數(shù),你把電話留給店員就叫登記回調(diào)函數(shù),店里后來有貨了叫做觸發(fā)了回調(diào)關(guān)聯(lián)的事件,店員給你打電話叫做調(diào)用回調(diào)函數(shù),你到店里去取貨叫做響應(yīng)回調(diào)事件?!?/p>
至于回調(diào)函數(shù)的官方定義是什么,這里就不展開了,畢竟和我們本篇文章關(guān)系不大。有興趣的小伙伴可以去知乎搜一下。
不友好的“回調(diào)地獄”寫過node代碼的小伙伴一定會遇到這樣的一個調(diào)用方式,比如下面mysql數(shù)據(jù)庫的查詢語句:
connection.query(sql1, (err, result) => { //ES6箭頭函數(shù) //第一次查詢 if(err) { console.err(err); } else { connection.query(sql2, (err, result) => { //第二次查詢 if(err) { console.err(err); } else { ... } }; } })
上面的代碼大概的意思是,使用mysql數(shù)據(jù)庫進(jìn)行查詢數(shù)據(jù),當(dāng)執(zhí)行完sql1語句之后,再執(zhí)行sql2語句。
可見,上面執(zhí)行sql1語句和sql2語句有一個先后的過程。為了實(shí)現(xiàn)先去執(zhí)行sql1語句再執(zhí)行sql2語句,我們只能這樣簡單粗暴的去嵌套調(diào)用。
如果只有兩三步操作還好,那么假如是十步操作或者更多,那代碼的結(jié)構(gòu)是不是更加的復(fù)雜了而且還難以閱讀。
所以,Promise就為了解決這個問題,而出現(xiàn)了。
promise用法這一部分的內(nèi)容絕大部分摘抄自《ES6標(biāo)準(zhǔn)入門》一書,如果你已經(jīng)讀過相關(guān)Promise的使用方法,那么你大可以快速瀏覽或直接跳過。
同時,你更需要留意一下catch部分和涉及“事件循環(huán)”的三個例子。
promise是什么? promise的定義Promise的三個狀態(tài)所謂Promise,簡單說就是一個容器,里面保存著某個未來才會結(jié)束的事件的結(jié)果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理,讓開發(fā)者不用再關(guān)注于時序和底層的結(jié)果。Promise的狀態(tài)具有不受外界影響和不可逆兩個特點(diǎn),與譯后的“承諾”這個詞有著相似的特點(diǎn)。
首先,Promise對象代表一個異步操作,有三種狀態(tài):pending(進(jìn)行中)、fulfilled(已成功)、rejected(已失敗)。
只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都沒有辦法改變這個狀態(tài)。
狀態(tài)不可逆其次,狀態(tài)是不可逆的。也就是說,一旦狀態(tài)改變,就不會再變成其他的了,往后無論何時,都可以得到這個結(jié)果。
對于Promise的狀態(tài)的改變,只有兩種情況:一是pending變成fulfilled,一是pending變成rejected。(注:下文用resolved指代fulfilled)
只要這兩種情況中的一種發(fā)生了,那么狀態(tài)就被固定下來了,不會再發(fā)生改變。
同時,如果改變已經(jīng)發(fā)生了,此時再對Promise對象指定回調(diào)函數(shù),那么會立即執(zhí)行添加的回調(diào)函數(shù),返回Promise的狀態(tài)。這與事件完全不同。事件的狀態(tài)是瞬時性的,一旦錯過,它的狀態(tài)將不會被保存。此時再去監(jiān)聽,肯定是得不到結(jié)果的。
Promise怎么用? promise的基本用法ES6規(guī)定,Promise對象是一個構(gòu)造函數(shù),用來生成Promise實(shí)例。
實(shí)例對象
這里,我們先來new一個全新的Promise實(shí)例。
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 異步操作成功*/) { resolve(value); } else { reject(error); } });
可以看到,Promise構(gòu)造函數(shù)接受一個匿名函數(shù)作為參數(shù),在函數(shù)中,又分別接受resolve和reject兩個參數(shù)。這兩個參數(shù)代表著內(nèi)置的兩個函數(shù)。
resovle的作用是,將Promise對象的狀態(tài)從“未完成(pending)”變?yōu)椤俺晒?resolved)”,通常在異步操作成功時調(diào)用,并將異步操作的結(jié)果,做為它的參數(shù)傳遞出去。
reject的作用是,將Promise對象的狀態(tài)從“未完成(pending)”變成"失敗(rejected)",通常在異步操作失敗時調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞出去。
接收狀態(tài)的回調(diào)
在Promise實(shí)例生成以后,可以使用then方法指定resolved狀態(tài)和rejected狀態(tài)。
//接上“實(shí)例對象”的代碼 promise.then(function(value) { //success },function(error) { //failure });
可見,then方法可以接受兩個回調(diào)函數(shù)作為參數(shù)。第一個回調(diào)函數(shù)是Promise對象的狀態(tài)變?yōu)?b>resolved時調(diào)用,第二個回調(diào)函數(shù)是promise對象的狀態(tài)變?yōu)?b>rejected時調(diào)用。其中,第二個函數(shù)是可選的。并不一定要提供。另外,這兩個函數(shù)都接受Promise對象傳出的值作為參數(shù)。
下面給出了一個簡單的例子:
function timeout(ms) { return new Promise((resolve, reject) { setTimeout(resolve, ms, "done"); }); } timeout(100).then(function(value) { console.log(value); //done });
上面的例子,是在100ms之后,把新建的Promise對象由pending狀態(tài)變?yōu)?b>resolved狀態(tài),接著觸發(fā)then方法綁定的回調(diào)函數(shù)。
另外,Promise在新建的時候就會立即執(zhí)行,因此我們也可以直接改變Promise的狀態(tài)。
//涉及“事件循環(huán)”例子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實(shí)例后立即執(zhí)行,所以首先輸出的是"Promise",僅接著resolve之后,觸發(fā)then的回調(diào)函數(shù),它將在當(dāng)前腳本所有同步任務(wù)執(zhí)行完了之后才會執(zhí)行,所以接下來輸出的是"Hi!",最后才是"resolved"。(注:這里涉及到JS的任務(wù)執(zhí)行過程和事件循環(huán),如果還不是很了解這個流程可以全部看完后再回過來理解一下這段代碼。)
關(guān)于Promise的基本用法,就先講解到這里。
接下來我們來看一下Promise封裝的原生方法。
Promise實(shí)例上的then和catchPromise.prototype.then
Promise的原型上有then方法,前面已經(jīng)提及和體驗(yàn)過,它的作用是為Promise實(shí)例添加狀態(tài)改變時的回調(diào)函數(shù)。 then的方法的第一個參數(shù)是resolved狀態(tài)的回調(diào)函數(shù),第二個參數(shù)(可選)是rejected狀態(tài)的回調(diào)函數(shù)。
then方法返回的是一個新的Promise實(shí)例,因此可以采用鏈?zhǔn)綄懛?,也就是說在then后面可以再調(diào)用另一個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方法,依次指定了兩個回調(diào)函數(shù)。第一個回調(diào)函數(shù)完成以后,會將返回結(jié)果作為參數(shù),傳入第二個回調(diào)函數(shù)。
也就是說,在Promise中傳參有兩種方式:
一是實(shí)例Promise的時候把參數(shù)通過resovle()傳遞出去。
二是在then方法中通過return返回給后面的then。
采用鏈?zhǔn)降?b>then,可以指定一組按照次序調(diào)用的回調(diào)函數(shù)。這時,前一個回調(diào)函數(shù),有可能返回的還是一個Promise對象(即有異步操作),這時后一個回調(diào)函數(shù),就會等待該Promise對象的狀態(tài)發(fā)生變化,才會被調(diào)用。
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方法指定的回調(diào)函數(shù),返回的是另一個Promise對象。這時,第二個then方法指定的回調(diào)函數(shù),就會等待這個新的Promise對象狀態(tài)發(fā)生變化。如果變?yōu)?b>resolved,就調(diào)用funcA,如果狀態(tài)變?yōu)?b>rejected,就調(diào)用funcB。
Promise.prototype.catch
Promise.prototype.catch方法是.then(null, rejection)的別名,用于指定發(fā)生錯誤時的回調(diào)函數(shù)。
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里面的兩個參數(shù)被剝離開來,如果異步操作拋出錯誤,就會調(diào)用catch方法指定的回調(diào)函數(shù),處理這個錯誤。
值得一提的是,現(xiàn)在我們在給rejected狀態(tài)綁定回調(diào)的時候,更傾向于catch的寫法,而不使用then方法的第二個參數(shù)。這種寫法,不僅讓Promise看起來更加簡潔,更加符合語義邏輯,接近try/catch的寫法。更重要的是,Promise對象的錯誤具有向后傳遞的性質(zhì)(書中說“冒泡”我覺得不是很合適,可能會誤解),直到錯誤被捕獲為止。
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產(chǎn)生,另外兩個由不同的兩個then產(chǎn)生。無論是其中的任何一個拋出錯誤,都會被最后一個catch捕獲。
如果還是對Promise錯誤向后傳遞的性質(zhì)不清楚,那么可以按照下面的代碼做一下實(shí)驗(yàn),便可以更加清晰的認(rèn)知這個特性。
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); //處理錯誤 })
以上,分別將1、2、3的位置進(jìn)行解注釋,就能夠證明我們以上的結(jié)論。
關(guān)于catch方法,還有三點(diǎn)需要提及的地方。
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出錯
所以,代碼出現(xiàn)了未捕獲的錯誤,這就是為什么我強(qiáng)調(diào)說是“向后傳遞錯誤而不是冒泡傳遞錯誤”。
在Promise沒有使用catch而拋出未處理的錯誤。
const someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行會報錯,因?yàn)閤沒有聲明 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函數(shù)產(chǎn)生的Promise 對象,內(nèi)部有語法錯誤。瀏覽器運(yùn)行到這一行,會打印出錯誤提示ReferenceError: x is not defined,但是不會退出進(jìn)程、終止腳本執(zhí)行,2秒之后還是會輸出123。這就是說,Promise內(nèi)部的錯誤不會影響到Promise外部的代碼,通俗的說法就是“Promise會吃掉錯誤”。
解決的方法就是在then后面接一個catch方法。
涉及到Promise中的異步任務(wù)拋出錯誤的時候。
//涉及“事件循環(huán)”例子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捕獲,結(jié)果就成了一個未捕獲的錯誤。
原因有二:
其一,由于在setTimeout之前已經(jīng)resolve過了,由于這個時候的Promise狀態(tài)就變成了resolved,所以它走的應(yīng)該是then而不是catch,就算后面再拋出錯誤,由于其狀態(tài)不可逆的原因,依舊不會拋出錯誤。也就是下面這種情況:
const promise = new Promise(function (resolve, reject) { resolve("ok"); throw new Error("test"); //依然不會拋出錯誤 }); //...省略
其二,setTimeout是一個異步任務(wù),它是在下一個“事件循環(huán)”才執(zhí)行的。當(dāng)?shù)搅讼乱粋€事件循環(huán),此時Promise早已經(jīng)執(zhí)行完畢了,此時這個錯誤并不是在Promise內(nèi)部拋出了,而是在全局作用域中,于是成了未捕獲的錯誤。(注:這里涉及到JS的任務(wù)執(zhí)行過程和事件循環(huán),如果還不是很了解這個流程可以全部看完后再回過來理解一下這段代碼。)
解決的方法就是直接在setTimeout的回調(diào)函數(shù)中去try/catch。
這個方法可以把現(xiàn)有的對象轉(zhuǎn)換成一個Promise對象,如下:
const jsPromise = Promise.resolve($.ajax("/whatever.json"));
上面代碼把jQuery中生成的deferred對象轉(zhuǎn)換成了一個新的Promise對象。
Promise的參數(shù)大致分下面四種:
如果參數(shù)是Promise實(shí)例,那么Promise.resolve將不做任何修改、原封不動地返回這個實(shí)例。
參數(shù)是一個thenable對象。
thenable對象指的是具有then方法的對象,比如下面這個對象。
let thenable = { then: function(resolve, reject) { resolve(42); } };
Promise.resolve方法會將這個對象轉(zhuǎn)為Promise對象,然后就立即執(zhí)行thenable對象的then方法,如下:
let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // 42 });
參數(shù)不是具有then方法的對象,或根本就不是對象。
如果參數(shù)是一個原始值,或者是一個不具有then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態(tài)為resolved。
不帶有任何參數(shù)。
Promise.resolve方法允許調(diào)用時不帶參數(shù),直接返回一個resolved狀態(tài)的Promise對象。
//涉及“事件循環(huán)”例子3 setTimeout(function () { console.log("three"); }, 0); Promise.resolve().then(function () { console.log("two"); }); console.log("one"); // one // two // three
上面這個例子,由于Promise算是一個微任務(wù),當(dāng)?shù)谝淮问录h(huán)執(zhí)行完了之后(console.log("one")),會取出任務(wù)隊(duì)列中的所有微任務(wù)執(zhí)行完(Promise.resovle().then),再進(jìn)行下一次事件循環(huán),也就是之后再執(zhí)行setTimeout。所以輸出的順序就是one、two、three。(注:這里涉及到JS的任務(wù)執(zhí)行過程和事件循環(huán),如果還不是很了解這個流程可以全部看完后再回過來理解一下這段代碼。)
Promise.rejectPromise.reject(reason)方法也會返回一個新的Promise實(shí)例,該實(shí)例的狀態(tài)為rejected,并立即執(zhí)行其回調(diào)函數(shù)。
注意,Promise.reject()方法的參數(shù),會原封不動地作為reject的理由,變成后續(xù)方法的參數(shù)。這一點(diǎn)與Promise.resolve方法不一致。
const thenable = { then(resolve, reject) { reject("出錯了"); } }; Promise.reject(thenable) .catch(e => { console.log(e === thenable) }); // true
上面代碼中,Promise.reject方法的參數(shù)是一個thenable對象,執(zhí)行以后,后面catch方法的參數(shù)不是reject拋出的“出錯了”這個字符串,而是thenable對象。
其他下面的方法只做簡單的介紹,如果需要更詳細(xì)的了解它,請到傳送門處查詢相關(guān)資料。
Promise.allPromise.all方法用于將多個Promise實(shí)例,包裝成一個新的Promise實(shí)例。
const p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.all方法接受一個數(shù)組作為參數(shù),p1、p2、p3都是Promise實(shí)例,如果不是,就會先調(diào)用上面講到的Promise.resolve方法,將參數(shù)轉(zhuǎn)為Promise實(shí)例,再進(jìn)一步處理。
p的狀態(tài)由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled,此時p1、p2、p3的返回值組成一個數(shù)組,傳遞給p的回調(diào)函數(shù)。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態(tài)就變成rejected,此時第一個被reject的實(shí)例的返回值,會傳遞給p的回調(diào)函數(shù)。
Promise.raceconst p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.race方法接受一個數(shù)組作為參數(shù),p1、p2、p3都是Promise實(shí)例,如果不是,就會先調(diào)用上面講到的Promise.resolve方法,將參數(shù)轉(zhuǎn)為Promise實(shí)例,再進(jìn)一步處理。
與Promise.all不同,只要其中有一個實(shí)例率先改變狀態(tài),p的狀態(tài)就跟著改變。那么率先改變的Promise實(shí)例的返回值,就傳遞給p的回調(diào)函數(shù)。
donePromise對象的回調(diào)鏈,不管以then方法或catch方法結(jié)尾,要是最后一個方法拋出錯誤,都有可能無法捕捉到。因此,我們可以提供一個done方法,總是處于回調(diào)鏈的尾端,保證拋出任何可能出現(xiàn)的錯誤。
它的實(shí)現(xiàn)代碼相當(dāng)簡單。
Promise.prototype.done = function (onFulfilled, onRejected) { this.then(onFulfilled, onRejected) .catch(function (reason) { // 拋出一個全局錯誤 setTimeout( function() { throw reason }, 0); }); };
從上面代碼可見,done方法的使用,可以像then方法那樣用,提供fulfilled和rejected狀態(tài)的回調(diào)函數(shù),也可以不提供任何參數(shù)。但不管怎樣,done都會捕捉到任何可能出現(xiàn)的錯誤,并向全局拋出。
finallyfinally方法用于指定不管Promise對象最后狀態(tài)如何,都會執(zhí)行的操作。它與done方法的最大區(qū)別,它接受一個普通的回調(diào)函數(shù)作為參數(shù),該函數(shù)不管怎樣都必須執(zhí)行。
下面是一個例子,服務(wù)器使用Promise處理請求,然后使用finally方法關(guān)掉服務(wù)器。
server.listen(0) .then(function () { // run test }); .finally(server.stop);
它的實(shí)現(xiàn)也非常的簡單。
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的Deferred對象
最初,在低版本的JQuery中,對于回調(diào)函數(shù),它的功能是非常弱的。無限“嵌套”回調(diào),編程起來十分不友好。為了改變這個問題,JQuery團(tuán)隊(duì)就設(shè)計了deferred對象。
它把回調(diào)的嵌套調(diào)用改寫成了鏈?zhǔn)秸{(diào)用,具體的寫法也十分的簡單。這里也不詳細(xì)講,想了解的小伙伴也可以直接到這個鏈接去看。傳送門
外部修改狀態(tài)但是,由于deferred對象它的狀態(tài)可以在外部被修改到,這樣會導(dǎo)致混亂的出現(xiàn),于是就有了deferred.promise。
它是在原來的deferred對象上返回另外一個deferred對象,后者只開放與改變執(zhí)行狀態(tài)無關(guān)的方法,屏蔽與改變執(zhí)行狀態(tài)有關(guān)的方法。從而來避免上述提到的外部修改狀態(tài)的情況。
如果有任何疑問,可以回到傳送門一看便知。
值得一提的是,JQuery中的Promise與我們文章講的Promise并沒有關(guān)系,只是名字一樣罷了。
雖然兩者遵循的規(guī)范不相同,但是都致力于一件事情,那就是:基于回調(diào)函數(shù)更好的編程方式。
promise編程結(jié)構(gòu) 返回新Promise既然我們學(xué)了Promise,那么就應(yīng)該在日常開發(fā)中去使用它。
然而,對于初學(xué)者來說,在使用Promise的時候,可能會出現(xiàn)嵌套問題。
比如說下面的代碼:
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... }); }); });
假如說現(xiàn)在需要p1、p2、p3按照順序執(zhí)行,那么剛?cè)腴T的小伙伴可能會這樣寫。
其實(shí)也沒有錯,這里是用了Promise,但是用得并不徹底,依然存在“回調(diào)”地獄,沒有深入到Promise的核心部分。
那么我們應(yīng)該怎么樣更好的去運(yùn)用它呢?
回顧一下前面Promise部分,你應(yīng)該可以得到答案。
下面,看我們修正后的代碼。
//同上,省略定義。 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){ //同一處理錯誤信息 });
可以看到,每次執(zhí)行完了then方法之后,我們都return了一個新的Promise。那么當(dāng)新的Promise中resolve之后,那么顯而易見的,它會執(zhí)行跟在它后面的then之中。
也就是說,在p1的then方法執(zhí)行完了之后,現(xiàn)在我們要去執(zhí)行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接收到了。
于是,基于這個結(jié)構(gòu),我們就可以在開發(fā)中去封裝出一個Promise供我們來使用。
在實(shí)例中使用剛好最近在做一個mysql的數(shù)據(jù)庫課設(shè),這里就把我如何封裝promise給貼出來。
下面的例子,可能有些接口剛接觸node的小伙伴會看不懂,那么,我會盡量的做到無死角注釋,大家也盡量關(guān)注一下封裝的過程(注:重點(diǎn)關(guān)注標(biāo)“*”的地方)。
首先是mysql.js封裝文件。
var mysql = require("mysql");//引入mysql庫 //創(chuàng)建一個連接池,同一個連接池可以同時存在多個連接,連接完成需要釋放 var pool = mysql.createPool({ ...//省略連接的配置 }); /** * 把mySQL查詢功能封裝成一個promise * @param String sql * @returns Promise **/ var QUERY = (sql) => { //注意這里new了一個新的promise(*) var connect = new Promise((resolve, reject) => { //創(chuàng)建連接 pool.getConnection((err, connection) => { //下面是狀態(tài)執(zhí)行(*) if (err) { reject(err);//如果創(chuàng)建連接失敗的話直接reject(*) } else { //否則可以進(jìn)行查詢了 connection.query(sql, (err, results) => { //執(zhí)行完查詢釋放連接 connection.release(); //在查詢的時候如果出錯直接reject if (err) { reject(err);//(*) } else { //否則成功,把查詢的結(jié)果resolve出去 //然后給后面then去使用 resolve(results);//(*) } }); } }); }); //最后把promise給return出去(*) return connect; }; module.exports = QUERY; //把封裝好的庫導(dǎo)出
接下來,去使用我們封裝好的查詢Promise。
假如我們現(xiàn)在想要使用查詢功能獲取某個數(shù)據(jù)表的所有數(shù)據(jù):
var QUERY = require("mysql"); //把我們寫的庫給導(dǎo)入 var sql = `SELECT * FROM student`;//sql語句,看不懂直接忽略 //執(zhí)行查詢操作 QUERY(sql).then((results) => { //(*) //這里就可以使用查詢到的results了 }).catch((err) => { //使用catch可以捕獲到整條鏈拋出的錯誤。(*) console.log(err); })
以上,就是一個實(shí)例了。所以以后,如果你想要封裝一個Promise來使用,你可以這樣來寫。
如何更優(yōu)雅的使用Promise?那么,現(xiàn)在問題又來了,如果我們現(xiàn)在需要進(jìn)行很多異步操作(比如Ajax通信),那么如果按照上面的寫法,會導(dǎo)致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; } // [] 用來保存初始化的值 相當(dāng)于聲明results = [] var pushValue = recordValue.bind(null, []); return request.comment() //位置1 .then(pushValue) .then(request.people) .then(pushValue); } // 運(yùn)行示例 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。
因此,如果我們將處理內(nèi)容統(tǒng)一放到數(shù)組里,再配合for循環(huán)進(jìn)行處理的話,那么處理內(nèi)容的增加將不會再帶來什么問題。首先我們就使用for循環(huán)來完成和前面同樣的處理。
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對象的函數(shù)的數(shù)組 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; } // 運(yùn)行示例 main().then(function (value) { console.log(value); }).catch(function(error){ console.error(error); });
使用for循環(huán)的時候,每次調(diào)用then都會返回一個新創(chuàng)建的Promise對象 因此類似promise = promise.then(task).then(pushValue);的代碼就是通過不斷對promise進(jìn)行處理,不斷的覆蓋 promise變量的值,以達(dá)到對Promise對象的累積處理效果。 但是這種方法需要promise這個臨時變量,從代碼質(zhì)量上來說顯得不那么簡潔。 如果將這種循環(huán)寫法改用Array.prototype.reduce的話,那么代碼就會變得聰明多了。
于是我們再對main函數(shù)進(jìn)行修改:
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第一個參數(shù)執(zhí)行數(shù)組每個值的回調(diào)函數(shù),第二個參數(shù)是初始值?;卣{(diào)函數(shù)中,第一個參數(shù)是上一次調(diào)用回調(diào)返回的值或提供的初始值,第二個是數(shù)組中正在處理的元素。)
最后,重寫完了整個函數(shù)就是:
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]); } // 運(yùn)行示例 main().then(function (value) { console.log(value); }).catch(function(error){ console.error(error); });
需要注意的是,在sequenceTasks中傳入的應(yīng)該是返回Promise對象的函數(shù)的數(shù)組,而不是一個Promise對象,因?yàn)橐坏┓祷匾粋€對象的時候,異步任務(wù)其實(shí)已經(jīng)是開始執(zhí)行了。
綜上,在寫順序隊(duì)列的時候,核心思想就是不斷的去return新的Promise并進(jìn)行狀態(tài)判斷 。而至于怎么寫,要根據(jù)實(shí)際情況進(jìn)行編程。
是回調(diào)不好還是嵌套不好?本質(zhì)上來說,回調(diào)本身沒有什么不好的,但是因?yàn)榛卣{(diào)的存在,使得我們無限的嵌套函數(shù)構(gòu)成了“回調(diào)地獄”,這對開發(fā)者來說無疑是特別不友好的。而雖然Promise只是回調(diào)的語法糖,但是卻提供給我們更好的書寫方式,解決了回調(diào)地獄嵌套的難題。
更多最后,這里算是一個拓展和學(xué)習(xí)方向,學(xué)習(xí)起來有一定的難度。
為什么JavaScript使用異步的方式來處理任務(wù)?由于JavaScript是一種單線程的語言,所謂的單線程就是按照我們書寫的代碼一樣一行一行的執(zhí)行下來,于是每次只能做一件事。
如果我們不是用異步的方式而用同步的方式去處理任務(wù),假如現(xiàn)在我們有一個網(wǎng)絡(luò)請求,請求后面是與其無關(guān)的一些操作代碼。那么當(dāng)請求發(fā)送出去的時候,由于現(xiàn)在執(zhí)行代碼是按部就班的,于是我們就必須等待網(wǎng)絡(luò)請求的應(yīng)答之后,我們才能繼續(xù)往下執(zhí)行我們的代碼。而這個等待,不僅花費(fèi)了我們很多時間。同時,也阻塞了我們后面的代碼。造成了不必要的資源浪費(fèi)。
于是,當(dāng)使用異步的方式來處理任務(wù)的時候,每次發(fā)送請求,JavaScript中的執(zhí)行棧會把異步操作交給瀏覽器的webCore內(nèi)核來處理,然后繼續(xù)往下執(zhí)行代碼。當(dāng)主線程的執(zhí)行棧代碼執(zhí)行完畢之后,就會去檢查任務(wù)隊(duì)列中有沒有任務(wù)需要執(zhí)行的。
如果有,則取出來到主線程的執(zhí)行棧中執(zhí)行,執(zhí)行完畢后,更新dom,然后再進(jìn)行一次同樣的循環(huán)。
而任務(wù)隊(duì)列中任務(wù)的添加,則是靠瀏覽器內(nèi)核webCore。每次異步操作完成之后,webCore內(nèi)核就會把相應(yīng)的回調(diào)函數(shù)添加到任務(wù)隊(duì)列中。
值得注意的是,任務(wù)隊(duì)列中任務(wù)按照任務(wù)性質(zhì)劃分為宏任務(wù)和微任務(wù)。而由于任務(wù)類型的不同,可能存在多個類型的任務(wù)隊(duì)列。但是事件循環(huán)只能有一個。
所以現(xiàn)在我們把宏任務(wù)和微任務(wù)考慮進(jìn)去,第一次執(zhí)行完腳本的代碼(算是一次宏任務(wù)),那么就會到任務(wù)隊(duì)列的微任務(wù)隊(duì)列中取出其所有任務(wù)放到主線程的執(zhí)行棧中來執(zhí)行,執(zhí)行完畢后,更新dom。下一次事件循環(huán),再從任務(wù)隊(duì)列中取出一個宏任務(wù),然后執(zhí)行微任務(wù)隊(duì)列中的所有微任務(wù)。再循環(huán)...
注:第一次執(zhí)行代碼的時候,就已經(jīng)開始了第一次的事件循環(huán),此時的script同步代碼是一個宏任務(wù)。
整個過程,也就是下面的這一個圖:
常見的異步任務(wù)有:網(wǎng)絡(luò)請求、IO操作、計時器和事件綁定等。
以上,如果你能夠看懂我在講什么,那么說明你真正理解了JS中的異步,如果不懂,那么你需要去了解一下“事件循環(huán)、任務(wù)隊(duì)列、宏任務(wù)與微任務(wù)”,下面是兩篇不錯的博客,值得學(xué)習(xí)。
事件循環(huán):http://www.ruanyifeng.com/blo...
對JS異步任務(wù)執(zhí)行的一個總結(jié):http://www.yangzicong.com/art...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/89763.html
摘要:或許你說你之前用過來做異步流程控制。那么作為一個程序好奇貓,你一定剖析過的源碼吧,很好奇它怎么使用來控制的同步。 promise + yield = 異步流程控制 異步計算已經(jīng)成為前后端不阻塞主線程的不二選擇,無論是增加性能或是提升用戶體驗(yàn),anyway,這年頭誰不用兩下并發(fā)呢? 既然說到異步那就不得不提 promise 了,這個新的語法糖雖然建立在 callback 之上,但也好歹止...
摘要:傳統(tǒng)的異步方法回調(diào)函數(shù)事件監(jiān)聽發(fā)布訂閱之前寫過一篇關(guān)于的文章,里邊寫過關(guān)于異步的一些概念。內(nèi)部函數(shù)就是的回調(diào)函數(shù),函數(shù)首先把函數(shù)的指針指向函數(shù)的下一步方法,如果沒有,就把函數(shù)傳給函數(shù)屬性,否則直接退出。 Generator函數(shù)與異步編程 因?yàn)閖s是單線程語言,所以需要異步編程的存在,要不效率太低會卡死。 傳統(tǒng)的異步方法 回調(diào)函數(shù) 事件監(jiān)聽 發(fā)布/訂閱 Promise 之前寫過一篇關(guān)...
摘要:所以僅用于簡化理解,快速入門,依然需要閱讀有深入研究的文章來加深對各種異步流程控制的方法的掌握。 原文地址:http://zodiacg.net/2015/08/javascript-async-control-flow/ 隨著ES6標(biāo)準(zhǔn)逐漸成熟,利用Promise和Generator解決回調(diào)地獄問題的話題一直很熱門。但是對解決流程控制/回調(diào)地獄問題的各種工具認(rèn)識仍然比較麻煩。最近兩天...
摘要:執(zhí)行,輸出,宏任務(wù)執(zhí)行結(jié)束。到此為止,第一輪事件循環(huán)結(jié)束。參考入門阮一峰系列之我們來聊聊一道關(guān)于應(yīng)用的面試題阿里前端測試題關(guān)于中函數(shù)的理解與應(yīng)用這一次,徹底弄懂執(zhí)行機(jī)制一個面試題原生的所有方法介紹附一道應(yīng)用場景題目異步流程控制 說明 最近在復(fù)習(xí) Promise 的知識,所以就做了一些題,這里挑出幾道題,大家一起看看吧。 題目一 const promise = new Promise((...
摘要:前文該系列下的前幾篇文章分別對不同的幾種異步方案原理進(jìn)行解析,本文將介紹一些實(shí)際場景和一些常見的面試題。流程調(diào)度里比較常見的一種錯誤是看似串行的寫法,可以感受一下這個例子判斷以下幾種寫法的輸出結(jié)果辨別輸出順序這類題目一般出現(xiàn)在面試題里。 前文 該系列下的前幾篇文章分別對不同的幾種異步方案原理進(jìn)行解析,本文將介紹一些實(shí)際場景和一些常見的面試題。(積累不太夠,后面想到再補(bǔ)) 正文 流程調(diào)度...
閱讀 2646·2021-10-08 10:04
閱讀 2744·2021-09-06 15:02
閱讀 831·2019-08-30 13:50
閱讀 1560·2019-08-30 13:21
閱讀 2596·2019-08-30 11:15
閱讀 2123·2019-08-29 17:19
閱讀 1590·2019-08-26 13:55
閱讀 1268·2019-08-26 10:15