當前位置:
首頁 > 知識 > JavaScript非同步與Promise實現

JavaScript非同步與Promise實現

本文已獲熊建剛授權分享,文章篇幅較長,還請耐心觀看


在閱讀本文之前,你應該已經了解JavaScript非同步實現的幾種方式:回調函數,發布訂閱模式,Promise,生成器(Generator),其實還有async/await方式,這個後續有機會會介紹。本篇將介紹Promise,讀完你應該了解什麼是Promise,為什麼使用Promise,而不是回調函數,Promise怎麼使用,使用Promise需要注意什麼,以及Promise的簡單實現。


前言


如果你已經對JavaScript非同步有一定了解,或者已經閱讀過本系列的其他兩篇文章,那請繼續閱讀下一小節,若你還有疑惑或者想了解JavaScript非同步機制與編程,可以閱讀一遍這兩篇文章:


JavaScript之非同步編程簡述

JavaScript之非同步編程


回調函數


回調函數,作為JavaScript非同步編程的基本單元,非常常見,你肯定對下面這類代碼一點都不陌生:


上面這些代碼,一層一層,嵌套在一起,這種代碼通常稱為回調地獄,無論是可讀性,還是代碼順序,或者回調是否可信任,亦或是異常處理角度看,都是不盡人意的,下面做簡單闡述。


順序性


上文例子中代碼funcB函數,還有兩個定時器回調函數,回調內各自又有一個ajax非同步請求然後在請求回調裡面執行最外層傳入的回調函數,對於這類代碼,你是否能明確指出個回調的執行順序呢?如果funcB函數內還有非同步任務呢?,情況又如何?


假如某一天,比如幾個月後,線上出了問題,我們需要跟蹤非同步流,找出問題所在,而跟蹤這類非同步流,不僅需要理清個非同步任務執行順序,還需要在眾多回調函數中不斷地跳躍,調試(或許你還能記得諸如funcB這些函數的作用和實現),無論是出於效率,可讀性,還是出於人性化,都不希望開開發者們再經歷這種痛苦。


信任問題


如上,我們調用了一個第三方支付組件的支付API,進行購買支付,正常情況發現一切運行良好,但是假如某一天,第三方組件出問題了,可能多次調用傳入的回調,也可能傳回錯誤的數據。說到底,這樣的回調嵌套,控制權在第三方,對於回調函數的調用方式、時間、次數、順序,回調函數參數,還有下一節將要介紹的異常和錯誤都是不可控的,因為無論如何,並不總能保證第三方是可信任的。


錯誤處理

關於JavaScript錯誤異常,初中級開發接觸的可能並不多,但是其實還是有很多可以學習實踐的地方,如前端異常監控系統的設計,開發和部署,並不是三言兩語能闡述的,之後會繼續推出相關文章。


錯誤堆棧


我們知道當JavaScript拋出錯誤或異常時,對於未捕獲異常,瀏覽器會默認在控制台輸出錯誤堆棧信息,如下,當test未定義時:


輸出如圖:

JavaScript非同步與Promise實現



如圖中自頂向下輸出紅色異常堆棧信息,Uncaught表示該異常未捕獲,ReferenceError表明該異常類型為引用異常,冒號後是異常的詳細信息:test is not defined,test未定義;後面以at起始的行就是該異常發生處的調用堆棧。第一行說明異常發生在init函數,第二行說明init函數的調用環境,此處在控制台直接調用,即相當於在匿名函數環境內調用。


非同步錯誤堆棧


上面例子是同步代碼執行的異常,當異常發生在非同步任務內時,又會如何呢?,假如把上例中代碼放在一個setTimeout定時器內執行:


如圖:

JavaScript非同步與Promise實現



可以看到,非同步任務中的未捕獲異常,也會在控制台輸出,但是setTimeout非同步任務回調函數沒有出現在異常堆棧,為什麼呢?這是因為當init函數執行時,setTimeout的非同步回調函數不在執行棧內,而是通過事件隊列調用。


JavaScript錯誤處理


JavaScript的異常捕獲,主要有兩種方式:


第一種,try{}catch(e){}主動捕獲異常;

JavaScript非同步與Promise實現



如上,對於同步執行大代碼出現異常,try{}catch(e){}是可以捕獲的,那麼非同步錯誤呢?

JavaScript非同步與Promise實現


如上圖,我們發現,非同步回調中的異常無法被主動捕獲,由瀏覽器默認處理,輸出錯誤信息。


第二種,window.onerror事件處理器,所有未捕獲異常都會自動進入此事件回調


如上圖,輸出了script error錯誤信息,同時,你也許注意到了,控制台依然列印出了錯誤堆棧信 息,或許你不希望用戶看到這麼醒目的錯誤提醒,那麼可以使window.onerror的回調返回true即可阻止瀏覽器的默認錯誤處理行為:

JavaScript非同步與Promise實現


JavaScript非同步與Promise實現



當然,一般不隨意設置window.onerror回調,因為程序通常可能需要部署前端異常監控系統,而通常就是使用window.onerror處理器實現全局異常監控,而該事件處理器只能註冊一個回調。


回調與PROMISE

以上我們談到的諸多關於回調的不足,都很常見,所以必須是需要解決的,而Promise正是一種很好的解決這些問題的方式,當然,現在已經提出了比Promise更先進的非同步任務處理方式,但是目前更大範圍使用,兼容性更好的方式還是Promise,也是本篇要介紹的,之後會繼續介紹其他處理方式。


Promises/A+


分析了一大波問題後,我們知道Promise的目標是非同步管理,那麼Promise到底是什麼呢?


非同步,表示在將來某一時刻執行,那麼Promise也必須可以表示一個將來值;


非同步任務,可能成功也可能失敗,則Promise需要能完成事件,標記其狀態值(這個過程即決議-resolve,下文將詳細介紹);


可能存在多重非同步任務,即非同步任務回調中有非同步任務,所以Promise還需要支持可重複使用,添加非同步任務(表現為順序鏈式調用,註冊非同步任務,這些非同步任務將按註冊的順序執行)。


所以,Promise是一種封裝未來值的易於復用的非同步任務管理機制。


為了更好的理解Promise,我們介紹一下Promises/A+,一個公開的可操作的Promises實現標準。先介紹標準規範,再去分析具體實現,更有益於理解。


Promise代表一個非同步計算的最終結果。使用promise最基礎的方式是使用它的then方法,該方法會註冊兩個回調函數,一個接收promise完成的最終值,一個接收promise被拒絕的原因。


PROMISES/A

你可能還會想問Promises/A是什麼,和Promises/A+有什麼區別。Promises/A+在Promises/A議案的基礎上,更清晰闡述了一些準則,拓展覆蓋了一些事實上的行為規範,同時刪除了一些不足或者有問題的部分。


Promises/A+規範目前只關注如何提供一個可操作的then方法,而關於如何創建,決議promises是日後的工作。


術語


promise: 指一個擁有符合規範的then方法的對象;


thenable: 指一個定義了then方法的對象;


決議(resolve): 改變一個promise等待狀態至已完成或被拒絕狀態, 一旦決議,不再可變;


值(value): 一個任意合法的JavaScript值,包括undefined,thenable對象,promise對象;


exception/error: JavaScript引擎拋出的異常/錯誤


拒絕原因(reject reason): 一個promise被拒絕的原因


PROMISE狀態

一個promise只可能處於三種狀態之一:


等待(pending):初始狀態;


已完成(fulfilled):操作成功完成;


被拒絕(rejected):操作失敗;


這三個狀態變更關係需滿足以下三個條件:


處於等待(pending)狀態時,可以轉變為已完成(fulfilled)或者被拒絕狀態(rejected);


處於已完成狀態時,狀態不可變,且需要有一個最終值;


處於被拒絕狀態時,狀態不可變,且需要有一個拒絕原因。


THEN方法


一個promise必須提供一個then方法,以供訪問其當前狀態,或最終值或拒絕原因。

參數


該方法接收兩個參數,如promise.then(onFulfilled, onRejected):


兩個參數均為可選,均有默認值,若不傳入,則會使用默認值;


兩個參數必須是函數,否則會被忽略,使用默認函數;


onFulfilled: 在promise已完成後調用且僅調用一次該方法,該方法接受promise最終值作參數;


onRejected: 在promise被拒絕後調用且僅調用一次該方法,該方法接受promise拒絕原因作參數;


兩個函數都是非同步事件的回調,符合JavaScript事件循環處理流程


返回值


該方法必須返回一個promise:


決議過程(RESOLUTION)

決議是一個抽象操作過程,該操作接受兩個輸入:一個promise和一個值,可以記為;[[resolve]](promise, x),如果x是一個thenable對象,則嘗試讓promise參數使用x的狀態值;否則,將使用x值完成傳入的promise,決議過程規則如下:


如果promise和x引用自同一對象,則使用一個TypeError原因拒絕此promise;


x為Promise,則promise直接使用x的狀態;


x為對象或函數:


獲取一個x.then的引用;


若獲取x.then時拋出異常e,使用該e作為原因拒絕promise;


否則將該引用賦值給then;


若then是一個函數,就調用該函數,其作用域為x,並傳遞兩個回調函數參數,第一個是resolvePromise,第二個是rejectPromise:


若調用了resolvePromise(y),則執行resolve(promise, y);


若調用了rejectPrtomise(r),則使用原因r拒絕promise;


若多次調用,只會執行第一次調用流程,後續調用將被忽略;


若調用then拋出異常e,則:


若promise已決議,即調用了resolvePromise或rejectPrtomise,則忽略此異常;


否則,使用原因e拒絕promise;


若then不是函數,則使用x值完成promise;


若x不是對象或函數,則使用x完成promise。


自然,以上規則可能存在遞歸循環調用的情況,如一個promsie被一個循環的thenable對象鏈決議,此時自然是不行的,所以規範建議進行檢測,是否存在遞歸調用,若存在,則以原因TypeError拒絕promise。


Promise


在ES6中,JavaScript已支持Promise,一些主流瀏覽器也已支持該Promise功能,如Chrome,先來看一個Promsie使用實例:


輸出如下:


構造器


創建promise語法如下:


參數


一個函數,該函數接受兩個參數:resolve函數和reject函數;當實例化Promise構造函數時,將立即調用該函數,隨後返回一個Promise對象。通常,實例化時,會初始一個非同步任務,在非同步任務完成或失敗時,調用resolve或reject函數來完成或拒絕返回的Promise對象。另外需要注意的是,若傳入的函數執行拋出異常,那麼這個promsie將被拒絕。


靜態方法


Promise.all(iterable)


all方法接受一個或多個promsie(以數組方式傳遞),返回一個新promise,該promise狀態取決於傳入的參數中的所有promsie的狀態:


當所有promise都完成是,返回的promise完成,其最終值為由所有完成promsie的最終值組成的數組;


當某一promise被拒絕時,則返回的promise被拒絕,其拒絕原因為第一個被拒絕promise的拒絕原因;


輸出如下:


Promise.race(iterable)


race方法返回一個promise,只要傳入的諸多promise中的某一個完成或被拒絕,則該promise同樣完成或被拒絕,最終值或拒絕原因也與之相同。


Promise.resolve(x)


resolve方法返回一個已決議的Promsie對象:


若x是一個promise或thenable對象,則返回的promise對象狀態同x;


若x不是對象或函數,則返回的promise對象以該值為完成最終值;


否則,詳細過程依然按前文Promsies/A+規範中提到的規則進行。


該方法遵循Promise/A+決議規範。


Promsie.reject(reason)


返回一個使用傳入的原因拒絕的Promise對象。


實例方法


我們通過兩個例子介紹then方法,首先看第一個實例:


輸出如下:


輸出兩行信息:我們發現第二個then方法接收到的最終值是undefined,為什麼呢?看看第一個then方法調用後返回的promise狀態如下:


如上圖,發現調用第一個then方法後,返回promise最終值為undefined,傳遞給第二個then的回調,如果把上面的例子稍加改動:


輸出如下:


這次兩個then方法的回調都接收到了最終值,正如我們前文所說,』then』方法返回一個新promise,並且該新promise根據其傳入的回調執行的返回值,進行決議,而函數未明確return返回值時,默認返回的是undefined,這也是上面實例第二個then方法的回調接收undefined參數的原因。


這裡使用了鏈式調用,我們需要明確:共產生三個promise,初始promise,兩個then方法分別返回一個promise;而第一個then方法返回的新promise是第二個then方法的主體,而不是初始promise。


輸出如下圖:

JavaScript非同步與Promise實現



如圖中所輸出內容,我們需要明白以下幾點:


catch會為promise註冊拒絕回調函數,一旦非同步操作結束,調用了reject回調函數,則依次執行註冊的拒絕回調;


另外有一點和then方法相似,catch方法返回的新promise將使用其回調函數執行的返回值進行決議,如promise2,promise3狀態均為完成(resolved),但是promise3最終值為undefined,而promise2最終值為successed,這是因為在調用promise.catch方法時,傳入的回調沒有顯式的設置返回值;


對於promise4,由於調用catch方法時,回調中throw拋出異常,所以promise4狀態為拒絕(rejected),拒絕原因為拋出的異常;


特別需要注意的是這裡一共有四個promise,一旦決議,它們之間都是獨立的,我們需要明白無論是then方法,還是catch方法,都會返回一個新promise,此新promise與初始promise相互獨立。


catch方法和then方法的第二個參數一樣,都是為promise註冊拒絕回調。


鏈式調用


和jQuery的鏈式調用一樣,Promise設計也支持鏈式調用,上一步的返回值作為下一步方法調用的主體:


錯誤處理


我們前文提到了JavaScript非同步回調中的異常是難以處理的,而Promise對非同步異常和錯誤的處理是比較方便的:


輸出如圖,執行test拋出異常,導致promise被拒絕,拒絕原因即拋出的異常,然後執行catch方法註冊的拒絕回調:


決議,完成與拒絕


目前為止,關於Promise是什麼,我們應該有了一定的認識,這裡,需要再次說明的是Promise的三個重要概念及其關係:決議(resolve),完成(fulfill),拒絕(reject)。


完成與拒絕是Promise可能處於的兩種狀態;


決議是一個過程,是Promise由等待狀態變更為完成或拒絕狀態的一個過程;


靜態方法Promise.resolve描述的就是一個決議過程,而Promise構造函數,傳入的回調函數的兩個參數:resolve和reject,一個是完成函數,一個是拒絕函數,這裡令人疑惑的是為什麼這裡依然使用resolve而不是fulfill,我們通過一個例子解釋這個問題:


輸出如圖:


上例中,在創建一個Promise時,給resolve函數傳遞的是一個拒絕Promise,此時我們發現promise狀態是rejected,所以這裡第一個參數函數執行,完成的是一個更接近決議的過程(可以參考前文講述的決議過程),所以命名為resolve是更合理的;而第二個參數函數,則只是拒絕該promise:


reject函數並不會處理參數,而只是直接將其當做拒絕原因拒絕promise。


Promise實現


Promise是什麼,怎麼樣使用就介紹到此,另外一個問題是面試過程中經常也會被提及的:如何實現一個Promise,當然,限於篇幅,我們這裡只講思路,不會長篇大論。


構造函數


首先創建一個構造函數,供實例化創建promise,該構造函數接受一個函數參數,實例化時,會立即調用該函數,然後返回一個Promise對象:


靜態方法


在實例化創建Promise時,我們會將構造函數的兩個靜態方法:resolve和reject傳入初始函數,接下來需要實現這兩個函數:


還有另外兩個靜態方法,原理還是一樣,就不細說了。


實例方法


目前構造函數,和靜態方法完成和拒絕Promise都已經實現,接下來需要考慮的是Promise的實例方法和鏈式調用:


實例


以上可以簡單實現Promise部分非同步管理功能:


本篇由回調函數起,介紹了回調處理非同步任務的常見問題,然後介紹Promises/A+規範及Promise使用,最後就Promise實現做了簡單闡述(之後有機會會詳細實現一個Promise),花費一周終於把基本知識點介紹完,下一篇將介紹JavaScript非同步與生成器實現。


參考


Promises/A+ specification


JavaScript Promise


作者:熊建剛


原文:http://blog.codingplayboy.com/2017/05/10/async_promise/


---- 廣告----


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 JavaScript 的精彩文章:

如何看待浙江大學申請雙向綁定技術專利
程序員的計劃和變化

TAG:JavaScript |