在以前的異步JavaScript與Promise一文中,我介紹了Promise以及它在異步JavaScript中的使用意義。通常來講,咱們是經過各類JavaScript庫來應用Promise的。隨着使用Promise的機會愈來愈多,你也可能像我這樣會關心Promise究竟是如何工做的。顯然,瞭解Promise的實現細節,能夠幫助咱們更好地應用它。尤爲是碰到一些Promise的問題時,也許能夠更快速、更準確地定位緣由,並解決它。segmentfault
很是慶幸,在[Promises/A wiki][]中位於庫列表第一位的[Q][],提供了它做爲一個Promise庫的[基本設計原理解析][]。本文將主要根據Q的這篇文章,探討Promise的實現細節。數組
儘管Promise已經有本身的規範,但目前的各種Promise庫,在Promise的實現細節上是有差別的,部分API甚至在乎義上徹底不一樣。但Promise的核心內容,是相通的,它就是then
方法。在相關術語中,promise
指的就是一個有then
方法,且該方法能觸發特定行爲的對象或函數。promise
有關Promise核心說明的細節,推薦閱讀[Promises/A+][]。這篇文章是寫給Promise庫的開發者的,你能夠找到各類對Promise特性的說明。[Promises/A+][]但願開發者聽從這些特性,以實現能夠共同使用的Promise(也就是說,不一樣的Promise庫也可共用)。app
Promise能夠有不一樣的實現方式,所以Promise核心說明並不會討論任何具體的實現代碼。異步
先閱讀Promise核心說明的意思是:看,這就是須要寫出來的結果,請參照這個結果想想怎麼用代碼寫出來吧。ide
回想一下Promise解決的是什麼問題?回調。例如,函數doMission1()
表明第一件事情,如今,咱們想要在這件事情完成後,再作下一件事情doMission2()
,應該怎麼作呢?函數
先看看咱們常見的回調模式。doMission1()
說:「你要這麼作的話,就把doMission2()
交給我,我在結束後幫你調用。」因此會是:工具
doMission1(doMission2);
Promise模式又是如何呢?你對doMission1()
說:「不行,控制權要在我這裏。你應該改變一下,你先返回一個特別的東西給我,而後我來用這個東西安排下一件事。」這個特別的東西就是Promise,這會變成這樣:post
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
方法的對象。
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
生成的對象同時擁有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和其餘值區分開來。
接下來會是至關重要的一步。到前面到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...)