譯者: 辣椒炒肉html
原文地址:pouchdb.com/...git
JavaScript 開發者們,認可一個事實吧:咱們也許並不瞭解Promise。程序員
衆所周知,A+規範所定義的Promise,很是棒。github
有個很大的問題是,在我過去使用Promise的一年中,看到不少開發者們,在用 PouchDB API或者是其餘的Promise API,可是卻不理解其中的原理。編程
不相信麼?那請看我最近發佈的這個推特。(惋惜連接失效了,辣椒本人也沒看到)api
doSomething().then(function () {
return doSomethingElse();
});
doSomething().then(function () {
doSomethingElse();
});
doSomething().then(doSomethingElse());
doSomething().then(doSomethingElse);
複製代碼
若是你知道答案,那麼恭喜你!你是一個Promise大神!下面的內容能夠不看了。數組
至於其餘99.99%的人,也恭喜大家了,大家上對車了。我在推特上發佈的那個問題,尚未人給我一個完美的回答。我對本身的#3回答也感到難以想象,即便我寫了測試。promise
答案在這篇文章的最後。但首先,我想探討一下,爲何Promise如此棘手?爲何這麼多人都爲它所困惑?我也將提供一些解釋,相信會讓Promise不那麼難理解。瀏覽器
首先咱們嘗試一些假設。bash
若是你讀過一些Promise的文章,你確定會找到不少回調地獄的引用,它們穩定地延伸到屏幕的右邊,這很糟糕!
Promise 確實能夠解決這個問題,但它不只僅是起到縮進的做用。正如它被盛譽的那樣:回調地獄的救贖。回調函數帶來的問題就是,剝奪了咱們對return和throw的掌控,並且還有一個反作用,一個函數意外地調用了另一個函數。
事實上,回調函數還作了更加讓人討厭的事情:它丟失了原來的棧,這是咱們 在編程語言中一般認爲理所固然的事情。編寫代碼丟失了對棧的掌控,就像駕駛一輛沒有剎車的汽車那樣,你不知道它會駛向哪裏。
Promise的重點是,把異步所丟失的return, throw, 和棧還給咱們。可是你必須知道如何正確使用promises才能利用它們。
有的人試圖將Promise解釋爲卡通,或者這樣形容:「哦,這個返回值就是異步回來的結果」
我以爲這種解釋不是頗有幫助。對我來講,Promises都是關於代碼結構和流程的。因此我認爲,最好回顧一些常見的錯誤而且想一想怎麼修復它們。我稱之爲「菜鳥錯誤」的意思是:「你如今是一個菜鳥,但你很快會成爲一個職業選手」
Promise對不少人來講意味着不一樣的東西,但就本文而言,我只談論官方規範,在現代瀏覽器中暴露爲window.Promise
基於Promise的PouchDB,看看下面一個糟糕的例子:
remotedb.allDocs({
include_docs: true,
attachments: true
}).then(function (result) {
var docs = result.rows;
docs.forEach(function(element) {
localdb.put(element.doc).then(function(response) {
alert("Pulled doc with id " + element.doc._id + " and added to local db.");
}).catch(function (err) {
if (err.name == 'conflict') {
localdb.get(element.doc._id).then(function (resp) {
localdb.remove(resp._id, resp._rev).then(function (resp) {
// ...
複製代碼
若是你認爲這樣的寫法只限於初學者,那你錯了。我在官方的BlackBerry開發者博客中發現了這樣的代碼!舊的回調習慣很難消亡。(致上面代碼的做者:抱歉,但您的代碼頗有借鑑意義)
更好的例子是這樣:
remotedb.allDocs(...).then(function (resultOfAllDocs) {
return localdb.put(...);
}).then(function (resultOfPut) {
return localdb.get(...);
}).then(function (resultOfGet) {
return localdb.put(...);
}).catch(function (err) {
console.log(err);
});
複製代碼
這是Promise鏈式寫法,只有前一個promise執行完後面一個纔會執行,並將前一個的返回值做爲參數。稍後會詳細介紹。
這是大多數人對Promise的理解開始崩潰的地方。一旦用到他們熟悉的forEach和while循環時,他們就不知道怎麼和Promise一塊兒使用。因此他們這樣寫:
// 我想刪除所有的doc
db.allDocs({include_docs: true}).then(function (result) {
result.rows.forEach(function (row) {
db.remove(row.doc);
});
}).then(function () {
// 我天真地覺得我刪除了所有的doc
});
複製代碼
這段代碼有什麼問題?其實第一個函數返回undefined。這意味着第二個函數不會等待所有執行完db.remove(), 事實上它啥也不用等待。
這是一個很隱蔽的錯誤,由於PouchDB若是足夠快刪除這些文檔並更新UI, 你可能不會注意到任何錯誤。這個錯誤可能會在奇怪的條件下或者某些瀏覽器中暴露。這時候來調試幾乎是不可能的。
全部這些for/forEach/while都不是合適的解決辦法,這時候你須要Promise.all()
db.allDocs({include_docs: true}).then(function (result) {
return Promise.all(result.rows.map(function (row) {
return db.remove(row.doc);
}));
}).then(function (arrayOfResults) {
// 如今這些doc真的所有被刪除了!
});
複製代碼
這裏發生了什麼?Promise.all接受一個promise數組做爲參數,而後當每一個promsie都resolve了,返回一個新的promise,包括了每一個promise的resolve結果。它是for循環的異步等價物。
Promise.all()還將一個結果數組傳遞給下一個函數,這可能很是有用。例如,當你試圖從PouchDB中獲取多個東西, 若是任何一個子promise被rejected,那麼all()的promise也會被拒絕,這更有用。
這也是一個常見的錯誤。自信地認爲他們的代碼不會發生異常。不幸的是,這意味着任何的錯誤都會被吞下,你甚至都不會在控制檯中看到它們,這纔是最痛苦的。
爲了不這種狀況,我已經習慣在promise鏈中添加這樣的代碼:
somePromise().then(function () {
return anotherPromise();
}).then(function () {
return yetAnotherPromise();
}).catch(console.log.bind(console)); // <-- this is badass
複製代碼
即便你很是肯定不會發生任何錯誤,最好仍是添加一個catch(),讓生活更美好。
【辣椒沒看懂這段。大概是,用Promise封裝異步的操做吧(這不是很常規的操做麼)】
new Promise(function (resolve, reject) {
fs.readFile('myfile.txt', function (err, file) {
if (err) {
return reject(err);
}
resolve(file);
});
}).then(/** */);
複製代碼
【辣椒不喜歡上面這樣寫。我本身會封裝起來這段,return這個promise在別的地方await 這個promise獲取返回。我知道我在說es7的async/await, 我就是看不慣這種寫法。】
下面這段代碼有什麼問題?
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// 哎呀,我但願someOtherPromise()已經resolved了!
// 劇透:並無
});
複製代碼
正如我以前所說,Promise的魔力在於它們將咱們的return和throw帶回來。 但這在實踐中其實是什麼樣的?
每一個promise都會給你一個then()方法(或者catch(),是語法糖,能夠在then的第二個參數處理錯誤then(null,...))。 這裏咱們在then()函數內:
somePromise().then(function () {
// 我在then裏面
});
複製代碼
咱們在這兒能夠作三件事:
返回另外一個promise
返回一個同步值(或者是undefined)
拋出一個同步異常
一旦你理解了這個技巧,你就理解了Promise。 下面咱們具體說說這三點:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// 我拿到了一個用戶帳號!
});
複製代碼
請注意,我正在返回第二個promise。 return相當重要!! 若是我沒有寫return,那麼getUserAccountById()其實是effect,而下一個函數將接收undefined而不是userAccount。
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // 返回一個同步值!
}
return getUserAccountById(user.id); // 返回一個promise!
}).then(function (userAccount) {
// 我拿到了一個userAccount!
});
複製代碼
是否是很棒!第二個函數不關心userAccount是同步仍是異步獲取的。第一個函數能夠自由返回同步或異步值。
不幸的是,有一個事實是,JavaScript中的非返回函數在技術上返回undefined,這意味着當你想要返回一些內容時,很容易意外地引入effect。
出於這個緣由,我老是習慣在then()函數內return或throw,建議你也這樣作。
getUserByName('nolan').then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!'); // 拋出一個同步異常!
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // 返回一個同步值!
}
return getUserAccountById(user.id); // 返回一個promise!
}).then(function (userAccount) {
// 我拿到了userAccount!
}).catch(function (err) {
// 砰! 我拿到一個異常!
});
複製代碼
若是用戶註銷,咱們的catch()將收到同步錯誤,若是任何promise被拒絕,它將收到異步錯誤。 一樣,該函數不關心它得到的錯誤是同步仍是異步。
這特別有用,由於它能夠幫助識別開發過程當中的編碼錯誤。 例如,若是在then()函數內部的任何一點,咱們執行JSON.parse(),若是JSON無效,它可能會拋出同步錯誤。 有了回調,這個錯誤就會被吞噬,可是使用promise,咱們能夠在catch()函數中簡單地處理它。
好的,如今你已經學會了一些基本的promise技巧,那咱們就聊聊邊緣狀況吧。
我將這些錯誤歸類爲「高級」,由於我只看到了那些已經至關擅長Promise的程序員犯的錯誤。 可是,若是咱們但願可以解決我在本文開頭提出的問題,咱們還須要繼續討論一下。
上面我已經講過,promises對於將異步代碼包裝爲同步代碼很是有用。 可是,若是你發現本身會這樣寫:
new Promise(function (resolve, reject) {
resolve(/** 同步值*/);
}).then(/* ... */);
複製代碼
你可使用Promise.resolve()更簡潔地這樣寫:
Promise.resolve(/** 同步值*/).then(/* ... */);
複製代碼
這對於捕獲任何同步錯誤也很是有用。 它很是有用,我養成了幾乎全部promise-api都寫return的習慣:
function somePromiseAPI() {
return Promise.resolve().then(function () {
doSomethingThatMayThrow();
return 'foo';
}).then(/* ... */);
}
複製代碼
請記住,任何可能throw同步錯誤的代碼,均可能會發生「難以調試」的吞噬錯誤。若是你將全部的代碼都包裝在Promise.resolve()中,那麼就老是能夠確保稍後捕獲到。
相似地,有一個Promise.reject()可用於返回當即拒絕的promise:
Promise.reject(new Error('some awful error'));
複製代碼
我上面說過catch()只是語法糖。 因此這兩個片斷是等價的:
somePromise().catch(function (err) {
// handle error
});
somePromise().then(null, function (err) {
// handle error
});
複製代碼
可是,這並不意味着如下兩個片斷是等價的:
somePromise().then(function () {
return someOtherPromise();
}).catch(function (err) {
// handle error
});
somePromise().then(function () {
return someOtherPromise();
}, function (err) {
// handle error
});
複製代碼
若是您想知道爲何它們不相同,請想一下,若是第一個函數拋出錯誤會發生什麼:
somePromise().then(function () {
throw new Error('oh noes');
}).catch(function (err) {
// 我捕獲了一個異常
});
somePromise().then(function () {
throw new Error('oh noes');
}, function (err) {
// 我沒有捕獲到異常
});
複製代碼
事實證實,當您使用then(resolveHandler,rejectHandler)格式時,若是由resolveHandler自己拋出,則rejectHandler實際上不會捕獲錯誤。【辣椒我的os:這好理解,resolveHandler和rejectHandler是同一級的,捕獲不到應該是合理的,rejectHandler只能捕獲somePromise發生的異常。因此,你能夠用catch啊!】
出於這個緣由,我已經習慣於永遠不要使用then()的第二個參數,而且老是更喜歡catch()。 例外的狀況是我在編寫異步Mocha測試時,我可能會編寫一個測試來確保拋出錯誤:
it('should throw an error', function () {
return doSomethingThatThrows().then(function () {
throw new Error('I expected an error!');
}, function (err) {
should.exist(err);
});
});
複製代碼
說到這一點,Mocha和Chai是測試Promise-API的很好的組合。 pouchdb-plugin-seed項目有一些示例測試能夠幫助你入門。
假設你想按順序依次執行一系列的promises。 也就是說,你想要像Promise.all()這樣的東西,但它不會並行執行promises。
你可能天真地寫這樣的東西:
function executeSequentially(promises) {
var result = Promise.resolve();
promises.forEach(function (promise) {
result = result.then(promise);
});
return result;
}
複製代碼
不幸的是,這不會按照你的想法執行。 傳遞給executeSequentially()的promise仍然會並行執行。
發生這種狀況的緣由是你根本不想操做數組裏的promise。 根據promise規範,一旦建立了promise,它就會開始執行。 因此你須要的是數組promise工廠:
function executeSequentially(promiseFactories) {
var result = Promise.resolve();
promiseFactories.forEach(function (promiseFactory) {
result = result.then(promiseFactory);
});
return result;
}
複製代碼
我知道你在想什麼:「這個Java程序員究竟是誰,爲何他在談論工廠呢?」 promise工廠很簡單,它只是一個返回promise的函數:
function myPromiseFactory() {
return somethingThatCreatesAPromise();
}
複製代碼
爲何這樣寫就有用?它起做用是由於promise工廠在被調用以前不會建立promise。 它的工做方式與當時的功能相同,實際上,它是一個東西!
若是你看一下上面的executeSequentially()函數,而後想象myPromiseFactory在result.then(...)中被替換,那麼但願你會靈光一閃,獲得promise啓蒙。
一般,一個promise將取決於另外一個promise,但咱們但願獲得兩個promise的輸出。 例如:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// 我也須要「user」對象!
});
複製代碼
想要成爲優秀的JavaScript開發人員並避免厄運的金字塔,咱們可能只是將用戶對象存儲在更高範圍的變量中:
var user;
getUserByName('nolan').then(function (result) {
user = result;
return getUserAccountById(user.id);
}).then(function (userAccount) {
// 好的,我拿到了user和userAccount
});
複製代碼
這是能夠的,但我我的以爲它有點笨拙。 我推薦的策略:放下你的先入之見,使用金字塔寫法:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id).then(function (userAccount) {
// 好的,我拿到了user和userAccount
});
});
複製代碼
或者你這樣寫:
function onGetUserAndUserAccount(user, userAccount) {
return doSomething(user, userAccount);
}
function onGetUser(user) {
return getUserAccountById(user.id).then(function (userAccount) {
return onGetUserAndUserAccount(user, userAccount);
});
}
getUserByName('nolan')
.then(onGetUser)
.then(function () {
// 在這一點上,doSomething()完成了,咱們又回到了縮進0
});
複製代碼
隨着你的promise代碼變得愈來愈複雜,可能會發現本身將愈來愈多的函數提取到命名函數中。 我發現這樣的代碼很是美觀,像這樣:
putYourRightFootIn()
.then(putYourRightFootOut)
.then(putYourRightFootIn)
.then(shakeItAllAbout);
複製代碼
這就是promise的所有。
最後,當我介紹上面的promise難題時,這就是我提到的錯誤。 這是一個很是深奧的用例,你可能永遠不會遇到,但它確實讓我感到驚訝。
你認爲此代碼打印出來的是什麼?
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
console.log(result);
});
複製代碼
若是你認爲它打印出來bar,你就錯了。 它實際上打印出foo!
發生這種狀況的緣由是由於當你傳遞then()一個非函數(例如一個promise)時,它實際上將它解釋爲then(null),這會致使前一個promise的結果失敗。 你能夠本身測試一下:
Promise.resolve('foo').then(null).then(function (result) {
console.log(result);
});
複製代碼
它仍會打印foo。
這實際上回到了我以前關於promise與promise工廠的觀點。 簡而言之,你能夠將promise直接傳遞給then()方法,但它不會按照您的想法執行。 then()應該接受一個函數,因此極可能你打算這樣作:
Promise.resolve('foo').then(function () {
return Promise.resolve('bar');
}).then(function (result) {
console.log(result);
});
複製代碼
這會如咱們所指望的那樣,打印bar。
因此只需提醒本身:將函數傳遞給then()!
如今咱們已經學會了全部關於promise的知識,咱們應該可以解決我在本文開頭提出的難題。【就是推特那個】
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);
複製代碼
答案:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
複製代碼
doSomething().then(function () {
doSomethingElse();
}).then(finalHandler);
複製代碼
答案:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(undefined)
|------------------|
複製代碼
doSomething().then(doSomethingElse())
.then(finalHandler);
複製代碼
答案:
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
finalHandler(resultOfDoSomething)
|------------------|
複製代碼
doSomething().then(doSomethingElse)
.then(finalHandler);
複製代碼
答案
doSomething
|-----------------|
doSomethingElse(resultOfDoSomething)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
複製代碼
若是這些答案仍然沒有起到做用,那麼我建議你從新閱讀帖子,或者定義doSomething()和doSomethingElse()方法,並在瀏覽器中自行嘗試。
【文章年代久遠,那時候es7還沒出,可是依然有些參考意義。如今異步編程的解決辦法,大可能是Promise+async/await, 即:用Promise封裝異步api(好比fs.readFile),在外部的async方法中await剛纔封裝好的方法,爽爽的!這將從新審視這篇文章的做者提出的一些異步寫法】