寫在前面:編程
這是一篇總結文章,但也能夠理解爲是一篇翻譯,主體脈絡參考自下面這篇文章:json
www.mattgreer.org/articles/pr…數組
若英文閱讀無障礙,牆裂推薦該文章的閱讀。promise
在平常寫代碼的過程當中,我很常常會用到 promises 語法。當我自覺得了解 promises 詳細用法時,卻在一次討論中被問住了:「你知道 promises 內部的實現過程是怎樣的麼?」 是的,回想起來,我只是知道該如何使用它,殊不知道其內部真正的實現原理。這篇文章正是我本身的關於 promises 的回顧與總結。若是你看完了整篇文章,但願你也會更加理解 promises 的實現與原理。瀏覽器
咱們將會從零開始,逐步實現一個本身的 promises。最終的代碼將會和 Promises/A+ 規範類似,而且將會明白 promises 在異步編程中的重要性。固然,本文會假設讀者已經擁有了關於 promises 的基礎知識。bash
讓咱們從最簡單的 promises 實現開始吧。當咱們想要將下面的代碼閉包
doSomething(function(value) {
console.log('Got a value:' + value);
});
複製代碼
轉變爲異步
doSomething().then(function(value) {
console.log('Got a value:' + value);
});
複製代碼
這個時候,咱們須要怎麼作呢?很是簡單的方式就是,將原來的 doSomething()
函數從原來的寫法異步編程
function doSomething(callback) {
var value = 42;
callback(value);
}
複製代碼
轉變爲以下這種 'promise' 寫法:函數
function doSomething() {
return {
then: function(callback) {
var value = 42;
callback(value);
}
};
}
複製代碼
上面只是一個 callback 寫法的一種語法糖包裝而已,看起來毫無心義。不過,這是個很是重要的轉變,咱們已經開始觸達了 promises 的一個核心理念:
Promises 捕獲最終值( eventual values ),並將其放入到一個 Object 中。
Ps: 這裏有必要解釋「最終值」的概念。它是異步函數的返回值,狀態是不肯定的,有可能成功,也有可能失敗(以下圖)。
關於 Promises 與最終值( eventual values ),下文會包含更多的討論。
上面簡單的改寫並不足以對 promise 的特性作任何的說明,讓咱們來定義一個真正的 promise 函數吧:
function Promise(fn) {
var callback = null;
this.then = function(cb) {
callback = cb;
};
function resolve(value) {
callback(value);
}
fn(resolve);
}
複製代碼
代碼解析:將then
的寫法拆分,同時引入了resolve
函數,方便處理 Promise 的傳入對象(函數)。同時,使用callback
做爲溝通then
函數與resolve
函數的橋樑。這個代碼實現,有一點 Promise 該有的樣子了,不是麼?
在此基礎上,咱們的doSomething()
函數將會寫成這種形式:
function doSomething() {
return new Promise(function(resolve) {
var value = 42;
resolve(value);
});
}
複製代碼
當咱們嘗試執行的時候,會發現執行會報錯。這是由於,在上面的代碼實現中,resolve()
會比then
更早被調用,此時的callback
仍是null
。爲了解決這個問題,咱們使用setTimeout
的方式 hack 一下:
function Promise(fn) {
var callback = null;
this.then = function(cb) {
callback = cb;
};
function resolve(value) {
// 強制此處的 callback 在 event loop 的下一個
// 迭代中調用,這樣 then()將會在其以前執行
setTimeout(function() {
callback(value);
}, 1);
}
fn(resolve);
}
複製代碼
通過這樣的修改以後,咱們的代碼將能夠成功運行。
咱們設想的實現,是能夠在異步狀況下也能夠正常工做的。可是此時的代碼,是很是脆弱的。只要咱們的then()
函數中包含有異步的狀況,那麼變量callback
將會再次變成null
。既然這個代碼這麼渣渣,爲何還要寫下來呢?由於上面的模式很方便咱們待會的拓展,同時,這個簡單的寫法,也可讓大腦對then
、resolve
的工做方式有一個初步的瞭解。下面咱們考慮在此基礎上作必定的改進。
Promises 是擁有狀態的,咱們須要先了解 Promises 中都有哪些狀態:
一個 promise 在等待最終值的時候,將會是 pending 狀態,當獲得最終值的時候,將會是 resolved 狀態。
當一個 promise 成功獲得最終值的時候,它將會一直保持這個值,不會再次 resolve。
(固然,一個 promise 的狀態也能夠是 rejected,下文會細述)
爲了將狀態引入到咱們的代碼實現中,咱們將原來的代碼改寫爲下面:
function Promise(fn) {
var state = 'pending';
//value 表示經過resolve函數傳遞的參數
var value;
//deferred 用於保存then()裏面的函數參數
var deferred;
function resolve(newValue) {
value = newValue;
state = 'resolved';
if(deferred) {
handle(deferred);
}
}
function handle(onResolved) {
if(state === 'pending') {
deferred = onResolved;
return;
}
onResolved(value);
}
this.then = function(onResolved) {
handle(onResolved);
};
fn(resolve);
}
複製代碼
這個代碼看起來更加複雜了。不過此時的代碼可讓調用方任意調用then()
方法,也能夠任意使用resolve()
方法了。它也能夠同時運行在同步、異步的狀況下。
代碼解析:代碼中使用了state
這個flag。同時,then()
與resolve()
將公共的邏輯提取到了一個新的函數handle()
中:
then()
比resolve()
更早被調用的時候,此時的狀態是 pending,對應的 value 值並無準備好。咱們將then()
裏面對應的回調參數保存在 deferred 中,方便 promise 在獲取到 resolved 的時候調用。resolve()
比then()
更早被調用的時候,此時的狀態設置爲 resolved,對應的 value 值也已經獲得。當then()
被調用的時候,直接調用then()
裏面對應的回調參數便可。then()
與resolve()
將公共的邏輯提取到了一個新的函數handle()
中,所以無論上面的兩個 case 誰被觸發,最終都會執行 handle 函數。若是你仔細看會發現,此時的setTimeout
已經不見了。咱們經過 state 的狀態控制,已經獲得了正確的執行順序。固然,下面的文章中,還有會使用到setTimeout
的時候。
經過使用 promise,咱們調用對應方法的順序將不會受到任何影響。只要符合咱們的需求,在任什麼時候刻調用
resolve()
比then()
都不會影響其內部邏輯。
此時,咱們能夠嘗試屢次調用then
方法,會發現每一次獲得的都是相同的 value 值。
var promise = doSomething();
promise.then(function(value) {
console.log('Got a value:', value);
});
promise.then(function(value) {
console.log('Got the same value again:', value);
});
複製代碼
在咱們平常針對 promises 的編程中,下面的鏈式模式是常見的:
getSomeData()
.then(filterTheData)
.then(processTheData)
.then(displayTheData);
複製代碼
getSomeData()
返回的是一個 promise,此時能夠經過調用then()
方法。但值得注意的是,第一個then()
方法的返回值也必須是一個 promise,這樣纔可讓咱們的鏈式 promises 一直延續下去。
then()
方法必須永遠返回一個 promise。
爲了實現這個目的,咱們將代碼作進一步的改造:
function Promise(fn) {
var state = 'pending';
var value;
var deferred = null;
function resolve(newValue) {
value = newValue;
state = 'resolved';
if(deferred) {
handle(deferred);
}
}
function handle(handler) {
if(state === 'pending') {
deferred = handler;
return;
}
if(!handler.onResolved) {
handler.resolve(value);
return;
}
var ret = handler.onResolved(value);
handler.resolve(ret);
}
this.then = function(onResolved) {
return new Promise(function(resolve) {
handle({
onResolved: onResolved,
resolve: resolve
});
});
};
fn(resolve);
}
複製代碼
呼啦~ 如今的代碼讓人看起來彷佛有點抓狂😩。哈哈哈,你是否會慶幸一開始的時候咱們代碼不是那麼複雜呢?這裏面真正的一個關鍵點在於:then()
方法永遠返回一個新的 promise。
doSomething().then(function(result){
console.log("first result : ", result);
return 88;
}).then(function(secondResult){
console.log("second result : ", secondResult);
return 99;
})
複製代碼
讓咱們來詳細看看第二個 promise 的 resolve 過程。它接收來自第一個 promise 的 value 值。詳細的過程發生在 handle()
方法的底部。入參handler
帶有兩個參數:一個是 onResolved
回調,一個是對resolve()
方法的引用。在這裏,每個新的 promise 都會有一個對內部方法resolve()
的拷貝以及對應的運行時閉包。這是鏈接第一個 promise 與第二個 promise 的橋樑。
在代碼中,咱們能夠獲得第一個 promise 的 value 值:
var ret = handler.onResolved(value);
複製代碼
在上面的例子中,handler.onResolved
表示的是:
function(result){
console.log("first result : ", result);
return 88;
}
複製代碼
也就是說,handler.onResolved
實際上返回的是第一個 promise 的 then 被調用時候的傳入參數(函數)。第一個 handler 的返回值被用於第二個 promise 的 resolve 傳入參數。
這就是整個鏈式 promise 的工做方式。
若是咱們想要將全部的 then 返回的結果,該怎麼作呢?咱們可使用一個數組,來存放每一次的返回值:
doSomething().then(function(result) {
var results = [result];
results.push(88);
return results;
}).then(function(results) {
results.push(99);
return results;
}).then(function(results) {
console.log(results.join(', ');
});
// the output is
//
// 42, 88, 99
複製代碼
promises 永遠 resolve 返回的是一個值。當你想要返回多個值的時候,能夠經過建立某些符合結構來實現(如數組、object等)。
then()
中的傳入參數(回調函數)是並非必填的。若是爲空,在鏈式 promise 中,將會返回前一個 promise 的返回值。
doSomething().then().then(function(result) {
console.log('got a result', result);
});
// the output is
//
// got a result 42
複製代碼
你能夠查看handle()
中的實現方式,當前一個 promise 沒有 then 的傳入參數的時候,它會 resolve 前一個 promise 的value 值:
if(!handler.onResolved) {
handler.resolve(value);
return;
}
複製代碼
咱們的鏈式 promise 實現,依然顯得有些簡單。這裏的 resolve 返回的是一個簡單的值。假如想要 resolve 返回的是一個新的 promise 呢?好比下面的方式:
doSomething().then(function(result) {
// doSomethingElse 返回的是一個promise
return doSomethingElse(result);
}).then(function(finalResult) {
console.log("the final result is", finalResult);
});
複製代碼
若是是這樣的狀況,那麼咱們上面的代碼彷佛沒法應對這樣的狀況。對於緊隨其後的那個 promise 而言,它獲得的 value 值將會是一個 promise。爲了獲得預期的值,咱們須要這樣作:
doSomething().then(function(result) {
// doSomethingElse 返回的是一個promise
return doSomethingElse(result);
}).then(function(anotherPromise) {
anotherPromise.then(function(finalResult) {
console.log("the final result is", finalResult);
});
});
複製代碼
OMG... 這樣的實現實在是太糟糕了。難道做爲使用者,我還要每一次都須要本身來手動書寫這些冗餘的代碼麼?是否能夠在 promise 代碼內部處理一下這些邏輯呢?實際上,咱們只須要在已有代碼中的 resolve()
中增長一點判斷便可:
function resolve(newValue) {
if(newValue && typeof newValue.then === 'function') {
newValue.then(resolve);
return;
}
state = 'resolved';
value = newValue;
if(deferred) {
handle(deferred);
}
}
複製代碼
上面的代碼邏輯中咱們看到,resolve()
中若是遇到的是 promise,將會一直迭代調用resolve()
。直到最後得到的值再也不是一個 promise,纔會依照已有的邏輯繼續執行。
還有一個值得注意的點:看看代碼中是如何斷定一個對象是否是具備 promise 屬性的?經過斷定這個對象是否有then
方法。這種斷定方法被稱爲 "鴨子類型"(咱們並不關心對象是什麼類型,究竟是不是鴨子,只關心行爲)。
這種寬鬆的界定方式,可使得具體的不一樣 promise 實現彼此之間有一個很好地兼容。
在鏈式 promise 章節中,咱們的實現已經相對而言是很是完整的。可是咱們並無討論到 promises 中的錯誤處理。
在 promise 的決議過程當中,若是發生了錯誤,那麼 promise 將會拋出一個拒絕決議,同時給出對應的理由。對於調用者,怎麼知道錯誤發生了呢?能夠經過 then()
方法的第二個傳入參數(函數):
doSomething().then(function(value) {
console.log('Success!', value);
}, function(error) {
console.log('Uh oh', error);
});
複製代碼
正如上面提到的,一個 promise 會從初始狀態 pending 轉換爲要麼是resolved 狀態,要麼是 rejected 狀態。這二者,只能有一個做爲最終的狀態。對應到
then()
的兩個參數,只有一個會被真正執行。
在 promise 內部實現中,一樣容許有一個reject()
函數來處理 reject 狀態,能夠看作是 resolve()
函數的孿生兄弟。此時,doSomething()
函數也將會被改寫爲支持錯誤處理的方式:
function doSomething() {
return new Promise(function(resolve, reject) {
var result = somehowGetTheValue();
if(result.error) {
reject(result.error);
} else {
resolve(result.value);
}
});
}
複製代碼
對於此,咱們的代碼該作如何的對應改造呢?來看代碼:
function Promise(fn) {
var state = 'pending';
var value;
var deferred = null;
function resolve(newValue) {
if(newValue && typeof newValue.then === 'function') {
newValue.then(resolve, reject);
return;
}
state = 'resolved';
value = newValue;
if(deferred) {
handle(deferred);
}
}
function reject(reason) {
state = 'rejected';
value = reason;
if(deferred) {
handle(deferred);
}
}
function handle(handler) {
if(state === 'pending') {
deferred = handler;
return;
}
var handlerCallback;
if(state === 'resolved') {
handlerCallback = handler.onResolved;
} else {
handlerCallback = handler.onRejected;
}
if(!handlerCallback) {
if(state === 'resolved') {
handler.resolve(value);
} else {
handler.reject(value);
}
return;
}
var ret = handlerCallback(value);
handler.resolve(ret);
}
this.then = function(onResolved, onRejected) {
return new Promise(function(resolve, reject) {
handle({
onResolved: onResolved,
onRejected: onRejected,
resolve: resolve,
reject: reject
});
});
};
fn(resolve, reject);
}
複製代碼
代碼解析:不只僅新增了一個reject()
函數,並且handle()
方法內部也增長了對 reject
的邏輯處理:經過對state
的判斷,來決定具體執行handler
的 reject
/resolved
。
上面的代碼,只對已知的錯誤進行了處理。當發生某些不可知錯誤的時候,一樣應該引起 rejection。須要在對應的處理函數中增長try...catch
:
首先是在resolve()
方法中:
function resolve(newValue) {
try {
// ... as before
} catch(e) {
reject(e);
}
}
複製代碼
一樣的,在 handle()
執行具體 callback
的時候,也可能發生未知的錯誤:
function handle(handler) {
// ... as before
var ret;
try {
ret = handlerCallback(value);
} catch(e) {
handler.reject(e);
return;
}
handler.resolve(ret);
}
複製代碼
有時候,對於 promises 的錯誤解讀,將會致使 promises 吞下錯誤。這是個常常坑開發者的點。
讓咱們來考慮這個例子:
function getSomeJson() {
return new Promise(function(resolve, reject) {
var badJson = "<div>uh oh, this is not JSON at all!</div>";
resolve(badJson);
});
}
getSomeJson().then(function(json) {
var obj = JSON.parse(json);
console.log(obj);
}, function(error) {
console.log('uh oh', error);
});
複製代碼
這段代碼將會如何進行呢?在then()
中的 resolve 執行的是對 JSON 的解析。它覺得可以執行,結果卻拋出了異常,由於傳入的 value 值並非 JSON 格式。咱們寫了一個 error callback 來捕獲這個錯誤。這樣是沒有問題,對吧?
不,結果可能並不符合你的指望。此時的 error callback 並不會觸發。結果將會是:控制檯上沒有任何的 log 輸出。這個錯誤就這樣被平靜地吞掉了。
爲何會這樣?由於咱們的錯誤發生在then()
的 resolve 回調內部,源碼上看是發生在 handle()
方法內部。這將會致使的是,then()
返回的新的 promise 將會被觸發 reject,而不是現有的這個 promise 會觸發 reject:
function handle(handler) {
// ... as before
var ret;
try {
ret = handlerCallback(value);
} catch(e) {
// 到達這裏,觸發的是handler.reject()
// 這是then()返回的新的promise的reject()
// 若是改爲 handler.onRejected(ex),將會觸發本promise的reject()
handler.reject(e);
return;
}
handler.resolve(ret);
}
複製代碼
若是將上面代碼中的catch
部分改寫成:handler.onRejected(ex);
將會觸發的是本 promise 的reject()
。但這就違背了 promises 的原則:
一個 promise 會從初始狀態 pending 轉換爲要麼是 resolved 狀態,要麼是 rejected 狀態。這二者,只能有一個做爲最終的狀態。對應到
then()
的兩個參數,只有一個會被真正執行。
由於已經觸發了 resolved 狀態,那麼久不可能再次觸發 rejected 狀態。錯誤是在具體執行 resolved 函數的時候發生的,那麼這個 error,將會被下一個 promise 捕獲。
咱們能夠這樣驗證:
getSomeJson().then(function(json) {
var obj = JSON.parse(json);
console.log(obj);
}).then(null, function(error) {
console.log("an error occured: ", error);
});
複製代碼
這多是 promises 中最坑人的一個點了。固然,只要理解了其中的原因,那麼就能夠很好地避免。爲了更好地體驗,咱們有什麼解決方法來規避這個坑呢?請看下一節:
大部分的 promise 庫都包含有一個 done()
方法。它實現的功能和then()
方法類似,只是很好的規避了剛剛提到的then()
的坑。
done()
方法能夠像then()
那樣被調用。二者之間主要有兩點不一樣:
done()
方法返回的不是一個 promisedone()
中的任何錯誤將不會被 promise 實現捕獲(直接拋出)在咱們的例子中,若是使用done()
方法,將會更加保險:
getSomeJson().done(function(json) {
// when this throws, it won't be swallowed
var obj = JSON.parse(json);
console.log(obj);
});
複製代碼
從 promise 中的 rejection 恢復是有可能的。若是在一個包含有 rejection 的 promise 中增長更多的then()
方法,那麼從這個then()
開始,將會延續鏈式 promise 的正常處理流程:
aMethodThatRejects().then(function(result) {
// won't get here
}, function(err) {
// since aMethodThatRejects calls reject()
// we end up here in the errback
return "recovered!";
}).then(function(result) {
console.log("after recovery: ", result);
}, function(err) {
// we won't actually get here
// since the rejected promise had an errback
});
// the output is
// after recovery: recovered!
複製代碼
在本文的開頭,咱們使用了一個 hack 來讓咱們的簡單代碼可以正確容許。還記得麼?使用了一個 setTimeout
。當咱們完善了對應的邏輯以後,這個 hack 就沒有再使用了。但事實是:Promises/A+ 規範要求 promise 決議必須是一步的。爲了實現這個需求,最簡單的作法就是再次使用 setTimeout
將咱們的handle()
方法包裝一層:
function handle(handler) {
if(state === 'pending') {
deferred = handler;
return;
}
setTimeout(function() {
// ... as before
}, 1);
}
複製代碼
很是簡單的實現。可是,實際上的 promises 庫並不傾向於使用setTimeout
。若是對應的庫是用於 NodeJS,那麼它們傾向於使用 process.nextTick
,若是對應的庫是用於瀏覽器,那麼它們傾向於使用setImmediate
。
具體的作法咱們知道了,可是爲何規範中會有這樣的要求呢?
爲了確保一致性與可信賴的執行過程。讓咱們考慮這樣的狀況:
var promise = doAnOperation();
invokeSomething();
promise.then(wrapItAllUp);
invokeSomethingElse();
複製代碼
上面的代碼會被怎樣執行呢?基於命名,你可能設想這個執行過程會是這樣的:invokeSomething()
-> invokeSomethingElse()
-> wrapItAllUp()
。但實際上,這取決於在咱們當前的實現過程當中,promise 的 resolve 過程是同步的仍是異步的。若是doAnOperation()
的 promise 執行過程是異步的,那麼其執行過程將會是設想的流程。若是doAnOperation()
的 promise 執行過程是同步的,它真實的執行過程將會是invokeSomething()
-> wrapItAllUp()
-> invokeSomethingElse()
。這時,可能會致使某些意想不到的後果。
所以,爲了確保一致性與可信賴的執行過程。promise 的 resolve 過程被要求是異步的,即便自己可能只是簡單的同步過程。這樣作,可讓全部的使用體驗都是一直的,開發者在使用過程當中,也再也不須要擔憂各類不一樣的狀況的兼容。
若是讀到了這裏,那麼能夠肯定是真愛了!咱們將 promises 的核心概念都講了一遍。固然,文章中的代碼實現,大部分都是簡陋的。可能也會和真正的代碼庫實現有必定的出入。但但願不妨礙您對總體 promises 的理解。更多的關於 promises 的實現細節(如:all()
、race
等),能夠查看更多的文檔與源碼實現。
當真正理解了 promises 的工做原理以及它的一些邊界狀況,我才真正喜歡上它。今後個人項目中關於 promises 的代碼也變得更加簡潔。關於 promises,還有不少內容值得去探討,本文只是一個開始。