本文大多數內容翻譯自該篇文章javascript
Promise能夠認爲是一種用來解決異步處理的代碼規範。常見的異步處理是使用回調函數,回調函數有兩種模式,同步的回調和異步的回調。通常回調函數指的是異步的回調。html
同步回調java
function add(a, b, callback) { callback(a + b) }
console.log('before');
add(1, 2, result => console.log('Result: ' + result);
console.log('after');
複製代碼
輸出結果爲: before Result:3 afterweb
異步回調api
function addAsync(a, b, callback) {
setTimeout( () => callback(a + b), 1000);
}
console.log('before');
addAsync(1, 2, result => console.log('Result: ' + result));
console.log('after');
複製代碼
輸出結果: before after Result: 3promise
然而回調函數有個著名的坑就是「callback hell」,好比:app
doSomething1(function(value1) {
doSomething2(function(value2) {
doSomething3(function(value3) {
console.log("done! The values are: " + [value1, value2, value3].join(','));
})
})
})
複製代碼
爲了等value1, value2, value3數據都準備好,必需要一層一層嵌套回調函數。若是一直嵌套下去,就造成了callback hell,不利於代碼的閱讀。異步
若是改用Promise的寫法,只要寫成以下方式就行。ide
doSomething1().then(function() {
return value1;
}).then(function(tempValue1) {
return [tempValue1, value2].join(',');
}).then(function(tempValue2) {
console.log("done! ", [tempValue2, value3].join(','));
});
複製代碼
能夠注意到,Promise其實是把回調函數從doSomething
函數中提取到了後面的then
方法裏面,從而防止多重嵌套的問題。函數
一個 Promise 對象表明一個目前還不可用,可是在將來的某個時間點能夠被解析的值。它要麼解析成功,要麼失敗拋出異常。它容許你以一種同步的方式編寫異步代碼。
Promise的實現是根據Promises/A+規範實現的。
對於Promise的基本使用和入門,能夠參考promise-book。這裏對Promise的使用作了比較詳細的介紹。
2.1 resolve & reject
Promise構造函數用來構造一個Promise對象,其中入參匿名函數中resolve
和reject
這兩個也都是函數。若是resolve
執行了,則觸發promise.then中成功的回調函數;若是reject
執行了,則觸發promise.then中拒絕的回調函數。
var promise = new Promise(function(resolve, reject) {
// IF 若是符合預期條件,調用resolve
resolve('success');
// ELSE 若是不符合預期條件,調用reject
reject('failure')
})
複製代碼
2.2 Fulfilled & Rejected
Promise對象一開始的值是Pending準備狀態。
執行了resolve()
後,該Promise對象的狀態值變爲onFulfilled狀態。
執行了reject()
後,該Promise對象的狀態值變爲onRejected狀態。
Promise對象的狀態值一旦肯定(onFulfilled或onRejected),就不會再改變。即不會從onFulfilled轉爲onRejected,或者從onRejected轉爲onFulfilled。
2.3 快捷方法
獲取一個onFulfilled狀態的Promise對象:
Promise.resolve(1);
// 等價於
new Promise((resolve) => resolve(1));
複製代碼
獲取一個onRejected狀態的Promise對象:
Promise.reject(new Error("BOOM"))
// 等價於
new Promise((resolve, reject)
=> reject(new Error("BOOM")));
複製代碼
更多快捷方法請參考Promise API。
Promise的異常捕獲有兩種方式:
then
匿名函數中的reject
方法catch
方法3.1 then中的reject方法捕獲異常
這種方法只能捕獲前一個Promise對象中的異常,即調用then
函數的Promise對象中出現的異常。
var promise = Promise.resolve();
promise.then(function() {
throw new Error("BOOM!")
}).then(function (success) {
console.log(success);
}, function (error) {
// 捕捉的是第一個then返回的Promise對象的錯誤
console.log(error);
});
複製代碼
但該種方法沒法捕捉當前Promise對象的異常,如:
var promise = Promise.resolve();
promise.then(function() {
return 'success';
}).then(function (success) {
console.log(success);
throw new Error("Another BOOM!");
}, function (error) {
console.log(error); // 沒法捕捉當前then中拋出的異常
});
複製代碼
3.2 catch捕獲異常
上述栗子若改寫成以下形式,最後追加一個catch函數,則能夠正常捕捉到異常。
var promise = Promise.resolve();
promise.then(function() {
return 'success';
}).then(function (success) {
console.log(success);
throw new Error("Another BOOM!");
}).catch(function (error) {
console.log(error); // 能夠正常捕捉到異常
});
複製代碼
catch
方法能夠捕獲到then
中拋出的錯誤,也能捕獲前面Promise拋出的錯誤。 所以建議都經過catch
方法捕捉異常。
var promise = Promise.reject("BOOM!");
promise.then(function() {
return 'success';
}).then(function (success) {
console.log(success);
throw new Error("Another BOOM!");
}).catch(function (error) {
console.log(error); // BOOM!
});
複製代碼
值得注意的是:catch
方法其實等價於then(null, reject)
,上面能夠寫成:
promise.then(function() {
return 'success';
}).then(function (success) {
console.log(success);
throw new Error("Another BOOM!");
}).then(null, function(error) {
console.log(error);
})
複製代碼
總結來講就是:
使用promise.then(onFulfilled, onRejected)
的話,在 onFulfilled
中發生異常的話,在onRejected
中是捕獲不到這個異常的。
在promise.then(onFulfilled).catch(onRejected)
的狀況下then
中產生的異常能在.catch
中捕獲
.then
和 .catch
在本質上是沒有區別的須要分場合使用。
瞭解一個東西最好的方式就是嘗試本身實現它,儘管可能不少地方不完整,但對理解內在的運行原理是頗有幫助的。
這裏主要引用了JavaScript Promises ... In Wicked Detail這篇文章的實現,如下內容主要是對該篇文章的翻譯。
4.1 初步實現
首先實現一個簡單的Promise對象類型。只包含最基本的then
方法和resolve
方法,reject
方法暫時不考慮。
function Promise(fn) {
// 設置回調函數
var callback = null;
// 設置then方法
this.then = function (cb) {
callback = cb;
};
// 定義resolve方法
function resolve(value) {
// 這裏強制resolve的執行在下一個Event Loop中執行
// 即在調用了then方法後設置完callback函數,否則callback爲null
setTimeout(function () {
callback(value);
}, 1);
}
// 運行new Promise時傳入的函數,入參是resolve
// 按照以前講述的,傳入的匿名函數有兩個方法,resolve和reject
fn(resolve);
}
function doSomething() {
return new Promise(function (resolve) {
var value = 42;
resolve(value);
});
}
// 調用本身的Promise
doSomething().then(function (value) {
console.log("got a value", value);
});
複製代碼
好了,這是一個很粗略版的Promise。這個實現連Promise須要的三種狀態都還沒實現。這個版本主要直觀展現了Promise的核心方法:then
和resolve
。
該版本若是then
異步調用的話,仍是會致使Promise中的callback爲null。
var promise = doSomething();
setTimeout(function() {
promise.then(function(value) {
console.log("got a value", value);
})}, 1);
複製代碼
後續經過加入狀態來維護Promise,就能夠解決這種問題。
4.2 Promise添加狀態
經過添加一個字段state
用來維護Promise的狀態,當執行了resolve
函數後,修改state
爲resolved
,初始state
是pendding
。
function Promise(fn) {
var state = 'pending'; // 維護Promise實例的狀態
var value;
var deferred; // 在狀態還處於pending時用於保存回調函數的引用
function resolve(newValue) {
value = newValue;
state = 'resolved';
if (deferred) {
// deferred 有值代表回調已經設置了,調用handle方法處理回調函數
handle(deferred);
}
}
// handle方法經過判斷state選擇如何執行回調函數
function handle(onResolved) {
// 若是還處於pending狀態,則先保存then傳入的回調函數
if (state === 'pending') {
deferred = onResolved;
return;
}
onResolved(value);
}
this.then = function (onResolved) {
// 對then傳入的回調函數,調用handle去執行回調函數
handle(onResolved);
};
fn(resolve);
}
function doSomething() {
return new Promise(function (resolve) {
var value = 42;
resolve(value);
});
}
doSomething().then(function (value) {
console.log("got a value", value);
});
複製代碼
加入了狀態後,能夠經過判斷狀態來解決調用前後順序的問題:
在resolve()
執行前調用then()
。代表這時尚未value處理好,這時的狀態就是pending
,此時先保留then()
傳入的回調函數,等調用resolve()
處理好value值後再執行回調函數,此時回調函數保存在deferred
中。
在resolve()
執行後調用then()
。代表這時value已經經過resolve()
處理完成了。當調用then()
時就能夠經過調用傳入的回調函數處理value值。
該版本的Promise咱們能夠隨意先調用resolve()
或pending()
,二者的順序對程序的執行不會形成影響了。
4.3 Promise添加調用鏈
Promise是能夠鏈式調用的,每次調用then()
後都返回一個新的Promise實例,所以要修改以前實現的then()
方法。
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;
}
// 若是then沒有傳入回調函數
// 則直接執行resolve解析value值
if (!handler.onResolved) {
handler.resolve(value);
return;
}
// 獲取前一個then回調函數中的解析值
var ret = handler.onResolved(value);
handler.resolve(ret);
}
// 返回一個新的Promise實例
// 該實例匿名函數中執行handle方法,該方法傳入一個對象
// 包含了傳入的回調函數和resolve方法的引用
this.then = function (onResolved) {
return new Promise(function (resolve) {
handle({
onResolved: onResolved, // 引用上一個Promise實例then傳入的回調
resolve: resolve
});
});
};
fn(resolve);
}
function doSomething() {
return new Promise(function (resolve) {
var value = 42;
resolve(value);
});
}
// 第一個then的返回值做爲第二個then匿名函數的入參
doSomething().then(function (firstResult) {
console.log("first result", firstResult);
return 88;
}).then(function (secondResult) {
console.log("second result", secondResult);
});
複製代碼
then
中是否傳入回調函數也是可選的,如:
doSomething().then().then(function(result) {
console.log('got a result', result);
});
複製代碼
在handle()
方法的實現中,若是沒有回調函數,直接解析已有的value值,該值是上一個Promise實例中調用resolve(value)
中傳入的。
if(!handler.onResolved) {
handler.resolve(value);
return;
}
複製代碼
若是回調函數中返回的是一個Promise對象而不是一個具體數值怎麼辦?此時咱們須要對返回的Promise調用then()
方法。
doSomething().then(function(result) {
// doSomethingElse returns a promise
return doSomethingElse(result);
}).then(function(anotherPromise) {
anotherPromise.then(function(finalResult) {
console.log("the final result is", finalResult);
});
});
複製代碼
每次這樣寫很麻煩,咱們能夠在咱們的Promise中的resole()
方法內處理掉這種狀況。
function resolve(newValue) {
// 經過判斷是否有then方法判斷其是不是Promise對象
if (newValue && typeof newValue.then === 'function') {
// 遞歸執行resolve方法直至解析出值出來,
// 經過handler.onResolved(value)解析出值,這裏handler.onResolve就是resolve方法
newValue.then(resolve);
return;
}
state = 'resolved';
value = newValue;
if (deferred) {
handle(deferred);
}
}
複製代碼
4.4 Promise添加reject處理
直至目前爲止,已經有了一個比較像樣的Promise了,如今添加一開始忽略的reject()
方法,使得咱們能夠這樣使用Promise。
doSomething().then(function(value) {
console.log('Success!', value);
}, function(error) {
console.log('Uh oh', error);
});
複製代碼
實現也很簡單,reject()
方法與resolve()
方法相似。
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);
}
}
// 添加的reject方法,這裏將Promise實例的狀態設爲rejected
function reject(reason) {
state = 'rejected';
value = reason;
if (deferred) {
handle(deferred);
}
}
function handle(handler) {
if (state === 'pending') {
deferred = handler;
return;
}
var handlerCallback;
// 添加state對於rejected狀態的判斷
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);
}
function doSomething() {
return new Promise(function (resolve, reject) {
var reason = "uh oh, something bad happened";
reject(reason);
});
}
// 調用栗子
doSomething().then(function (firstResult) {
// wont get in here
console.log("first result:", firstResult);
}, function (error) {
console.log("got an error:", error);
});
複製代碼
目前咱們的異常處理機制只能處理本身拋出的異常信息,對於其餘的一些異常信息是沒法正常捕獲的,如在resolve()
方法中拋出的異常。咱們對此作以下修改:
function resolve(newValue) {
try {
// ... as before
} catch(e) {
reject(e);
}
}
複製代碼
這裏經過添加try catch
手動捕獲可能出現的異常,並在catch
中調用reject()
方法進行處理。一樣對於回調函數,執行時也可能出現異常,也須要作一樣的處理。
function handle(deferred) {
// ... as before
var ret;
try {
ret = handlerCallback(value);
} catch(e) {
handler.reject(e);
return;
}
handler.resolve(ret);
}
複製代碼
上述完整的演示代碼請查看原文做者提供的fiddle。
4.4 Promise保證異步處理
到目前爲止,咱們的Promise已經實現了基本比較完善的功能了。這裏還有一點須要注意的是,Promise規範提出不論是resolve()
仍是reject()
,執行都必須保持異步處理。要實現這一點很簡單,只需作以下修改便可:
function handle(handler) {
if(state === 'pending') {
deferred = handler;
return;
}
setTimeout(function() {
// ... as before
}, 1);
}
複製代碼
問題是爲何要這麼處理?這主要是爲了保證代碼執行流程的一致性和可靠性。考慮以下栗子:
var promise = doAnOperation();
invokeSomething();
promise.then(wrapItAllUp);
invokeSomethingElse();
複製代碼
經過代碼的意圖應該是但願invokeSomething()
和invokeSomethingElse()
都執行完後,再執行回調函數wrapItAllUp()
。若是Promise的resolve()
處理不是異步的話,則執行順序變爲invokeSomething()
-> wrapItAllUp()
-> invokeSomethingElse()
,跟預想的產生不一致。
爲了保證這種執行順序的一致性,Promise規範要求resolve
必須是異步處理的。
到這一步,咱們的Promise基本像模像樣了。固然離真正的Promise還有一段差距,好比缺少了經常使用的便捷方法如all()
,race()
等。不過本例子實現的方法原本就是從理解Promise原理出發的,相信經過該例子對Promise原理會有比較深刻的瞭解。
參考