摘要:采用鏈式的,可以指定一組按照次序調用的回調函數。異步操作成功異步操作成功上面代碼中,第一個方法指定的回調函數,返回的是另一個對象。這時,第二個方法指定的回調函數,就會等待這個新的對象狀態發生變化。方法是的別名,用于指定發生錯誤時的回調函數。
好久沒有更新文章了,最近剛好遇到考試,而且一直在做數據庫課設。
本來這篇文章是上個星期想要分享給工作室的師弟師妹們的,結果因為考試就落下了。
其實我并不是很想寫Promise,畢竟現在更好的方式是結合await/async和Promise編寫異步代碼。但是,其實覺得Promise這個東西對于入門ES6,改善“回調地獄”有很大的幫助,那也算是回過頭來復習一下吧。
本文很多地方參考了阮一峰的《ES6標準入門》這一本書,因為學ES6,這本書是最好的,沒有之一。當然,整理的文章也有我自己的思路在,還有加上了自己的一些理解,適合入門ES6的小伙伴學習。
如果已經對Promise有一定的了解,但并沒有實際的用過,那么可以看一下在實例中使用和如何更加優雅的使用Promise一節。
另外,本文中有三個例子涉及“事件循環和任務隊列”(均已在代碼頭部標出),如果暫時不能理解,可以先學完Promise之后去了解最后一節的知識,然后再回來看,這樣小伙伴你應該就豁然開朗了。
引言 回調函數所謂回調,就是“回來調用”,這里拿知乎上“常溪玲”一個很形象的例子: “ 你到一個商店買東西,剛好你要的東西沒有貨,于是你在店員那里留下了你的電話,過了幾天店里有貨了,店員就打了你的電話,然后你接到電話后就到店里去取了貨。在這個例子里,你的電話號碼就叫回調函數,你把電話留給店員就叫登記回調函數,店里后來有貨了叫做觸發了回調關聯的事件,店員給你打電話叫做調用回調函數,你到店里去取貨叫做響應回調事件?!?/p>
至于回調函數的官方定義是什么,這里就不展開了,畢竟和我們本篇文章關系不大。有興趣的小伙伴可以去知乎搜一下。
不友好的“回調地獄”寫過node代碼的小伙伴一定會遇到這樣的一個調用方式,比如下面mysql數據庫的查詢語句:
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語句。
可見,上面執行sql1語句和sql2語句有一個先后的過程。為了實現先去執行sql1語句再執行sql2語句,我們只能這樣簡單粗暴的去嵌套調用。
如果只有兩三步操作還好,那么假如是十步操作或者更多,那代碼的結構是不是更加的復雜了而且還難以閱讀。
所以,Promise就為了解決這個問題,而出現了。
promise用法這一部分的內容絕大部分摘抄自《ES6標準入門》一書,如果你已經讀過相關Promise的使用方法,那么你大可以快速瀏覽或直接跳過。
同時,你更需要留意一下catch部分和涉及“事件循環”的三個例子。
promise是什么? promise的定義Promise的三個狀態所謂Promise,簡單說就是一個容器,里面保存著某個未來才會結束的事件的結果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統一的 API,各種異步操作都可以用同樣的方法進行處理,讓開發者不用再關注于時序和底層的結果。Promise的狀態具有不受外界影響和不可逆兩個特點,與譯后的“承諾”這個詞有著相似的特點。
首先,Promise對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)、rejected(已失?。?。
只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都沒有辦法改變這個狀態。
狀態不可逆其次,狀態是不可逆的。也就是說,一旦狀態改變,就不會再變成其他的了,往后無論何時,都可以得到這個結果。
對于Promise的狀態的改變,只有兩種情況:一是pending變成fulfilled,一是pending變成rejected。(注:下文用resolved指代fulfilled)
只要這兩種情況中的一種發生了,那么狀態就被固定下來了,不會再發生改變。
同時,如果改變已經發生了,此時再對Promise對象指定回調函數,那么會立即執行添加的回調函數,返回Promise的狀態。這與事件完全不同。事件的狀態是瞬時性的,一旦錯過,它的狀態將不會被保存。此時再去監聽,肯定是得不到結果的。
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封裝的原生方法。
Promise實例上的then和catchPromise.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); //處理錯誤 })
以上,分別將1、2、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對象,如下:
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.rejectPromise.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.allPromise.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.raceconst p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.race方法接受一個數組作為參數,p1、p2、p3都是Promise實例,如果不是,就會先調用上面講到的Promise.resolve方法,將參數轉為Promise實例,再進一步處理。
與Promise.all不同,只要其中有一個實例率先改變狀態,p的狀態就跟著改變。那么率先改變的Promise實例的返回值,就傳遞給p的回調函數。
donePromise對象的回調鏈,不管以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都會捕捉到任何可能出現的錯誤,并向全局拋出。
finallyfinally方法用于指定不管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的Deferred對象
最初,在低版本的JQuery中,對于回調函數,它的功能是非常弱的。無限“嵌套”回調,編程起來十分不友好。為了改變這個問題,JQuery團隊就設計了deferred對象。
它把回調的嵌套調用改寫成了鏈式調用,具體的寫法也十分的簡單。這里也不詳細講,想了解的小伙伴也可以直接到這個鏈接去看。傳送門
外部修改狀態但是,由于deferred對象它的狀態可以在外部被修改到,這樣會導致混亂的出現,于是就有了deferred.promise。
它是在原來的deferred對象上返回另外一個deferred對象,后者只開放與改變執行狀態無關的方法,屏蔽與改變執行狀態有關的方法。從而來避免上述提到的外部修改狀態的情況。
如果有任何疑問,可以回到傳送門一看便知。
值得一提的是,JQuery中的Promise與我們文章講的Promise并沒有關系,只是名字一樣罷了。
雖然兩者遵循的規范不相同,但是都致力于一件事情,那就是:基于回調函數更好的編程方式。
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來使用,你可以這樣來寫。
如何更優雅的使用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是一種單線程的語言,所謂的單線程就是按照我們書寫的代碼一樣一行一行的執行下來,于是每次只能做一件事。
如果我們不是用異步的方式而用同步的方式去處理任務,假如現在我們有一個網絡請求,請求后面是與其無關的一些操作代碼。那么當請求發送出去的時候,由于現在執行代碼是按部就班的,于是我們就必須等待網絡請求的應答之后,我們才能繼續往下執行我們的代碼。而這個等待,不僅花費了我們很多時間。同時,也阻塞了我們后面的代碼。造成了不必要的資源浪費。
于是,當使用異步的方式來處理任務的時候,每次發送請求,JavaScript中的執行棧會把異步操作交給瀏覽器的webCore內核來處理,然后繼續往下執行代碼。當主線程的執行棧代碼執行完畢之后,就會去檢查任務隊列中有沒有任務需要執行的。
如果有,則取出來到主線程的執行棧中執行,執行完畢后,更新dom,然后再進行一次同樣的循環。
而任務隊列中任務的添加,則是靠瀏覽器內核webCore。每次異步操作完成之后,webCore內核就會把相應的回調函數添加到任務隊列中。
值得注意的是,任務隊列中任務按照任務性質劃分為宏任務和微任務。而由于任務類型的不同,可能存在多個類型的任務隊列。但是事件循環只能有一個。
所以現在我們把宏任務和微任務考慮進去,第一次執行完腳本的代碼(算是一次宏任務),那么就會到任務隊列的微任務隊列中取出其所有任務放到主線程的執行棧中來執行,執行完畢后,更新dom。下一次事件循環,再從任務隊列中取出一個宏任務,然后執行微任務隊列中的所有微任務。再循環...
注:第一次執行代碼的時候,就已經開始了第一次的事件循環,此時的script同步代碼是一個宏任務。
整個過程,也就是下面的這一個圖:
常見的異步任務有:網絡請求、IO操作、計時器和事件綁定等。
以上,如果你能夠看懂我在講什么,那么說明你真正理解了JS中的異步,如果不懂,那么你需要去了解一下“事件循環、任務隊列、宏任務與微任務”,下面是兩篇不錯的博客,值得學習。
事件循環:http://www.ruanyifeng.com/blo...
對JS異步任務執行的一個總結:http://www.yangzicong.com/art...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89763.html
摘要:或許你說你之前用過來做異步流程控制。那么作為一個程序好奇貓,你一定剖析過的源碼吧,很好奇它怎么使用來控制的同步。 promise + yield = 異步流程控制 異步計算已經成為前后端不阻塞主線程的不二選擇,無論是增加性能或是提升用戶體驗,anyway,這年頭誰不用兩下并發呢? 既然說到異步那就不得不提 promise 了,這個新的語法糖雖然建立在 callback 之上,但也好歹止...
摘要:傳統的異步方法回調函數事件監聽發布訂閱之前寫過一篇關于的文章,里邊寫過關于異步的一些概念。內部函數就是的回調函數,函數首先把函數的指針指向函數的下一步方法,如果沒有,就把函數傳給函數屬性,否則直接退出。 Generator函數與異步編程 因為js是單線程語言,所以需要異步編程的存在,要不效率太低會卡死。 傳統的異步方法 回調函數 事件監聽 發布/訂閱 Promise 之前寫過一篇關...
摘要:所以僅用于簡化理解,快速入門,依然需要閱讀有深入研究的文章來加深對各種異步流程控制的方法的掌握。 原文地址:http://zodiacg.net/2015/08/javascript-async-control-flow/ 隨著ES6標準逐漸成熟,利用Promise和Generator解決回調地獄問題的話題一直很熱門。但是對解決流程控制/回調地獄問題的各種工具認識仍然比較麻煩。最近兩天...
摘要:執行,輸出,宏任務執行結束。到此為止,第一輪事件循環結束。參考入門阮一峰系列之我們來聊聊一道關于應用的面試題阿里前端測試題關于中函數的理解與應用這一次,徹底弄懂執行機制一個面試題原生的所有方法介紹附一道應用場景題目異步流程控制 說明 最近在復習 Promise 的知識,所以就做了一些題,這里挑出幾道題,大家一起看看吧。 題目一 const promise = new Promise((...
摘要:前文該系列下的前幾篇文章分別對不同的幾種異步方案原理進行解析,本文將介紹一些實際場景和一些常見的面試題。流程調度里比較常見的一種錯誤是看似串行的寫法,可以感受一下這個例子判斷以下幾種寫法的輸出結果辨別輸出順序這類題目一般出現在面試題里。 前文 該系列下的前幾篇文章分別對不同的幾種異步方案原理進行解析,本文將介紹一些實際場景和一些常見的面試題。(積累不太夠,后面想到再補) 正文 流程調度...
閱讀 2574·2021-10-08 10:04
閱讀 2735·2021-09-06 15:02
閱讀 792·2019-08-30 13:50
閱讀 1547·2019-08-30 13:21
閱讀 2587·2019-08-30 11:15
閱讀 2113·2019-08-29 17:19
閱讀 1574·2019-08-26 13:55
閱讀 1261·2019-08-26 10:15