摘要:顯然,了解的實現細節,可以幫助我們更好地應用它。本文將主要根據的這篇文章,探討的實現細節。核心說明盡管已經有自己的規范,但目前的各類庫,在的實現細節上是有差異的,部分甚至在意義上完全不同。到前面到為止,所實現的都是不能級聯的。
在之前的異步JavaScript與Promise一文中,我介紹了Promise以及它在異步JavaScript中的使用意義。一般來說,我們是通過各種JavaScript庫來應用Promise的。隨著使用Promise的機會越來越多,你也可能像我這樣會關心Promise到底是如何工作的。顯然,了解Promise的實現細節,可以幫助我們更好地應用它。尤其是碰到一些Promise的問題時,也許可以更快速、更準確地定位原因,并解決它。
非常慶幸,在[Promises/A wiki][]中位于庫列表第一位的[Q][],提供了它作為一個Promise庫的[基本設計原理解析][]。本文將主要根據Q的這篇文章,探討Promise的實現細節。
Promise核心說明盡管Promise已經有自己的規范,但目前的各類Promise庫,在Promise的實現細節上是有差異的,部分API甚至在意義上完全不同。但Promise的核心內容,是相通的,它就是then方法。在相關術語中,promise指的就是一個有then方法,且該方法能觸發特定行為的對象或函數。
有關Promise核心說明的細節,推薦閱讀[Promises/A+][]。這篇文章是寫給Promise庫的開發者的,你可以找到各種對Promise特性的說明。[Promises/A+][]希望開發者遵從這些特性,以實現可以共同使用的Promise(也就是說,不同的Promise庫也可共用)。
Promise可以有不同的實現方式,因此Promise核心說明并不會討論任何具體的實現代碼。
先閱讀Promise核心說明的意思是:看,這就是需要寫出來的結果,請參照這個結果想一想怎么用代碼寫出來吧。
起步:用這一種方式理解Promise回想一下Promise解決的是什么問題?回調。例如,函數doMission1()代表第一件事情,現在,我們想要在這件事情完成后,再做下一件事情doMission2(),應該怎么做呢?
先看看我們常見的回調模式。doMission1()說:“你要這么做的話,就把doMission2()交給我,我在結束后幫你調用。”所以會是:
doMission1(doMission2);
Promise模式又是如何呢?你對doMission1()說:“不行,控制權要在我這里。你應該改變一下,你先返回一個特別的東西給我,然后我來用這個東西安排下一件事。”這個特別的東西就是Promise,這會變成這樣:
doMission1().then(doMission2);
可以看出,Promise將回調模式的主從關系調換了一個位置(翻身做主人!),多個事件的流程關系,就可以這樣集中到主干道上(而不是分散在各個事件函數之內)。
好了,如何做這樣一個轉換呢?從最簡單的情況來吧,假定doMission1()的代碼是:
function doMission1(callback){ var value = 1; callback(value); }
那么,它可以改變一下,變成這樣:
function doMission1(){ return { then: function(callback){ var value = 1; callback(value); } }; }
這就完成了轉換。雖然并不是實際有用的轉換,但到這里,其實已經觸及了Promise最為重要的實現要點,即Promise將返回值轉換為帶then方法的對象。
進階:Q的設計路程 從defer開始design/q0.js是Q初步成型的第一步。它創建了一個名為defer的工具函數,用于創建Promise:
var defer = function () { var pending = [], value; return { resolve: function (_value) { value = _value; for (var i = 0, ii = pending.length; i < ii; i++) { var callback = pending[i]; callback(value); } pending = undefined; }, then: function (callback) { if (pending) { pending.push(callback); } else { callback(value); } } } };
這段源碼可以看出,運行defer()將得到一個對象,該對象包含resolve和then兩個方法。請回想一下jQuery的Deferred(同樣有resolve和then),這兩個方法將會是近似的效果。then會參考pending的狀態,如果是等待狀態則將回調保存(push),否則立即調用回調。resolve則將肯定這個Promise,更新值的同時運行完所有保存的回調。defer的使用示例如下:
var oneOneSecondLater = function () { var result = defer(); setTimeout(function () { result.resolve(1); }, 1000); return result; }; oneOneSecondLater().then(callback);
這里oneOneSecondLater()包含異步內容(setTimeout),但這里讓它立即返回了一個defer()生成的對象,然后將對象的resolve方法放在異步結束的位置調用(并附帶上值,或者說結果)。
到此,以上代碼存在一個問題:resolve可以被執行多次。因此,resolve中應該加入對狀態的判斷,保證resolve只有一次有效。這就是Q下一步的design/q1.js(僅差異部分):
resolve: function (_value) { if (pending) { value = _value; for (var i = 0, ii = pending.length; i < ii; i++) { var callback = pending[i]; callback(value); } pending = undefined; } else { throw new Error("A promise can only be resolved once."); } }
對第二次及更多的調用,可以這樣拋出一個錯誤,也可以直接忽略掉。
分離defer和promise在前面的實現中,defer生成的對象同時擁有then方法和resolve方法。按照定義,promise關心的是then方法,至于觸發promise改變狀態的resolve,是另一回事。所以,Q接下來將擁有then方法的promise,和擁有resolve的defer分離開來,各自獨立使用。這樣就好像劃清了各自的職責,各自只留一定的權限,這會使代碼邏輯更明晰,易于調整。請看design/q3.js:(q2在此跳過)
var isPromise = function (value) { return value && typeof value.then === "function"; }; var defer = function () { var pending = [], value; return { resolve: function (_value) { if (pending) { value = _value; for (var i = 0, ii = pending.length; i < ii; i++) { var callback = pending[i]; callback(value); } pending = undefined; } }, promise: { then: function (callback) { if (pending) { pending.push(callback); } else { callback(value); } } } }; };
如果你仔細對比一下q1,你會發現區別很小。一方面,不再拋出錯誤(改為直接忽略第二次及更多的resolve),另一方面,將then方法移動到一個名為promise的對象內。到這里,運行defer()得到的對象(就稱為defer吧),將擁有resolve方法,和一個promise屬性指向另一個對象。這另一個對象就是僅有then方法的promise。這就完成了分離。
前面還有一個isPromise()函數,它通過是否有then方法來判斷對象是否是promise(duck-typing的判斷方法)。為了正確使用和處理分離開的promise,會像這樣需要將promise和其他值區分開來。
實現promise的級聯接下來會是相當重要的一步。到前面到q3為止,所實現的promise都是不能級聯的。但你所熟知的promise應該支持這樣的語法:
promise.then(step1).then(step2);
以上過程可以理解為,promise將可以創造新的promise,且取自舊的promise的值(前面代碼中的value)。要實現then的級聯,需要做到一些事情:
then方法必須返回promise。
這個返回的promise必須用傳遞給then方法的回調運行后的返回結果,來設置自己的值。
傳遞給then方法的回調,必須返回一個promise或值。
design/q4.js中,為了實現這一點,新增了一個工具函數ref:
var ref = function (value) { if (value && typeof value.then === "function") return value; return { then: function (callback) { return ref(callback(value)); } }; };
這是在著手處理與promise關聯的value。這個工具函數將對任一個value值做一次包裝,如果是一個promise,則什么也不做,如果不是promise,則將它包裝成一個promise。注意這里有一個遞歸,它確保包裝成的promise可以使用then方法級聯。為了幫助理解它,下面是一個使用的例子:
ref("step1").then(function(value){ console.log(value); // "step1" return 15; }).then(function(value){ console.log(value); // 15 });
你可以看到value是怎樣傳遞的,promise級聯需要做到的也是如此。
design/q4.js通過結合使用這個ref函數,將原來的defer轉變為可級聯的形式:
var defer = function () { var pending = [], value; return { resolve: function (_value) { if (pending) { value = ref(_value); // values wrapped in a promise for (var i = 0, ii = pending.length; i < ii; i++) { var callback = pending[i]; value.then(callback); // then called instead } pending = undefined; } }, promise: { then: function (_callback) { var result = defer(); // callback is wrapped so that its return // value is captured and used to resolve the promise // that "then" returns var callback = function (value) { result.resolve(_callback(value)); }; if (pending) { pending.push(callback); } else { value.then(callback); } return result.promise; } } }; };
原來callback(value)的形式,都修改為value.then(callback)。這個修改后效果其實和原來相同,只是因為value變成了promise包裝的類型,會需要這樣調用。
then方法有了較多變動,會先新生成一個defer,并在結尾處返回這個defer的promise。請注意,callback不再是直接取用傳遞給then的那個,而是在此基礎之上增加一層,并把新生成的defer的resolve方法放置在此。此處可以理解為,then方法將返回一個新生成的promise,因此需要把promise的resolve也預留好,在舊的promise的resolve運行后,新的promise的resolve也會隨之運行。這樣才能像管道一樣,讓事件按照then連接的內容,一層一層傳遞下去。
加入錯誤處理promise的then方法應該可以包含兩個參數,分別是肯定和否定狀態的處理函數(onFulfilled與onRejected)。前面我們實現的promise還只能轉變為肯定狀態,所以,接下來應該加入否定狀態部分。
請注意,promise的then方法的兩個參數,都是可選參數。design/q6.js(q5也跳過)加入了工具函數reject來幫助實現promise的否定狀態:
var reject = function (reason) { return { then: function (callback, errback) { return ref(errback(reason)); } }; };
它和ref的主要區別是,它返回的對象的then方法,只會取第二個參數的errback來運行。design/q6.js的其余部分是:
var defer = function () { var pending = [], value; return { resolve: function (_value) { if (pending) { value = ref(_value); for (var i = 0, ii = pending.length; i < ii; i++) { value.then.apply(value, pending[i]); } pending = undefined; } }, promise: { then: function (_callback, _errback) { var result = defer(); // provide default callbacks and errbacks _callback = _callback || function (value) { // by default, forward fulfillment return value; }; _errback = _errback || function (reason) { // by default, forward rejection return reject(reason); }; var callback = function (value) { result.resolve(_callback(value)); }; var errback = function (reason) { result.resolve(_errback(reason)); }; if (pending) { pending.push([callback, errback]); } else { value.then(callback, errback); } return result.promise; } } }; };
這里的主要改動是,將數組pending只保存單個回調的形式,改為同時保存肯定和否定的兩種回調的形式。而且,在then中定義了默認的肯定和否定回調,使得then方法滿足了promise的2個可選參數的要求。
你也許注意到defer中還是只有一個resolve方法,而沒有類似jQuery的reject。那么,錯誤處理要如何觸發呢?請看這個例子:
var defer1 = defer(), promise1 = defer1.promise; promise1.then(function(value){ console.log("1: value = ", value); return reject("error happens"); }).then(function(value){ console.log("2: value = ", value); }).then(null, function(reason){ console.log("3: reason = ", reason); }); defer1.resolve(10); // Result: // 1: value = 10 // 3: reason = error happens
可以看出,每一個傳遞給then方法的返回值是很重要的,它將決定下一個then方法的調用結果。而如果像上面這樣返回工具函數reject生成的對象,就會觸發錯誤處理。
融入異步終于到了最后的design/q7.js。直到前面的q6,還存在一個問題,就是then方法運行的時候,可能是同步的,也可能是異步的,這取決于傳遞給then的函數(例如直接返回一個值,就是同步,返回一個其他的promise,就可以是異步)。這種不確定性可能帶來潛在的問題。因此,Q的后面這一步,是確保將所有then轉變為異步。
design/q7.js定義了另一個工具函數enqueue:
var enqueue = function (callback) { //process.nextTick(callback); // NodeJS setTimeout(callback, 1); // Na?ve browser solution };
顯然,這個工具函數會將任意函數推遲到下一個事件隊列運行。
design/q7.js其他的修改點是(只顯示修改部分):
var ref = function (value) { // ... return { then: function (callback) { var result = defer(); // XXX enqueue(function () { result.resolve(callback(value)); }); return result.promise; } }; }; var reject = function (reason) { return { then: function (callback, errback) { var result = defer(); // XXX enqueue(function () { result.resolve(errback(reason)); }); return result.promise; } }; }; var defer = function () { var pending = [], value; return { resolve: function (_value) { // ... enqueue(function () { value.then.apply(value, pending[i]); }); // ... }, promise: { then: function (_callback, _errback) { // ... enqueue(function () { value.then(callback, errback); }); // ... } } }; };
即把原來的value.then的部分,都轉變為異步。
到此,Q提供的Promise設計原理q0~q7,全部結束。
結語即便本文已經是這么長的篇幅,但所講述的也只到基礎的Promise。大部分Promise庫會有更多的API來應對更多和Promise有關的需求,例如all()、spread(),不過,讀到這里,你已經了解了實現Promise的核心理念,這一定對你今后應用Promise有所幫助。
在我看來,Promise是精巧的設計,我花了相當一些時間才差不多理解它。Q作為一個典型Promise庫,在思路上走得很明確。可以感受到,再復雜的庫也是先從基本的要點開始的,如果我們自己要做類似的事,也應該保持這樣的心態一點一點進步。
(重新編輯自我的博客,原文地址:http://acgtofe.com/posts/2015...)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/85560.html
摘要:當引擎開始執行一個函數比如回調函數時,它就會把這個函數執行完,也就是說只有執行完這段代碼才會繼續執行后面的代碼。當條件允許時,回調函數就會被運行。現在,返回去執行注冊的那個回調函數。 原文地址:http://blog.getify.com/promis... 在微博上看到有人分享LabJS作者寫的關于Promise的博客,看了下覺得寫得很好,分五個部分講解了Promise的來龍去脈。從...
摘要:有一個和相關的更大的問題。最后,請負有責任感并且使用安全的擴展。深入理解五部曲異步問題深入理解五部曲轉換問題深入理解五部曲可靠性問題深入理解五部曲擴展性問題深入理解五部曲樂高問題最后,安利下我的個人博客,歡迎訪問 原文地址:http://blog.getify.com/promis... 現在,我希望你已經看過深入理解Promise的前三篇文章了。并且假設你已經完全理解Promises...
摘要:簡單的說,即將到來的標準指出是一個,所以作為一個,必須可以被子類化。保護還是子類化這是個問題我真的希望我能創建一個忠實的給及以下。 原文地址:http://blog.getify.com/promis... 如果你需要趕上我們關于Promise的進度,可以看看這個系列前兩篇文章深入理解Promise五部曲--1.異步問題和深入理解Promise五部曲--2.控制權轉移問題。 Promi...
摘要:標準已于年月份正式定稿了,并廣泛支持最新的特性異步函數。為了領會,我們需要回到普通回調函數中進一步學習。從此編寫回調函數不再那么痛苦。回調是一個函數,可以將結果傳遞給函數并在該函數內進行調用,以便作為事件的響應。 ES2017標準已于2017年6月份正式定稿了,并廣泛支持最新的特性:異步函數。如果你曾經被異步 JavaScript 的邏輯困擾,這么新函數正是為你設計的。 異步函數或多或...
摘要:忍者級別的函數操作對于什么是匿名函數,這里就不做過多介紹了。我們需要知道的是,對于而言,匿名函數是一個很重要且具有邏輯性的特性。通常,匿名函數的使用情況是創建一個供以后使用的函數。 JS 中的遞歸 遞歸, 遞歸基礎, 斐波那契數列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執行機制 本文的目的就是要保證你徹底弄懂javascript的執行機制,如果...
閱讀 1642·2023-04-25 18:19
閱讀 2085·2021-10-26 09:48
閱讀 1092·2021-10-09 09:44
閱讀 1741·2021-09-09 11:35
閱讀 3034·2019-08-30 15:54
閱讀 2031·2019-08-30 11:26
閱讀 2295·2019-08-29 17:06
閱讀 892·2019-08-29 16:38