咱們也許並不瞭解Promise

譯者: 辣椒炒肉html

原文地址:pouchdb.com/...git

JavaScript 開發者們,認可一個事實吧:咱們也許並不瞭解Promise。程序員

衆所周知,A+規範所定義的Promise,很是棒。github

有個很大的問題是,在我過去使用Promise的一年中,看到不少開發者們,在用 PouchDB API或者是其餘的Promise API,可是卻不理解其中的原理。編程

不相信麼?那請看我最近發佈的這個推特。(惋惜連接失效了,辣椒本人也沒看到)api

問題:這四個Promise有什麼區別?

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的文章,你確定會找到不少回調地獄的引用,它們穩定地延伸到屏幕的右邊,這很糟糕!

Promise 確實能夠解決這個問題,但它不只僅是起到縮進的做用。正如它被盛譽的那樣:回調地獄的救贖。回調函數帶來的問題就是,剝奪了咱們對return和throw的掌控,並且還有一個反作用,一個函數意外地調用了另一個函數。

事實上,回調函數還作了更加讓人討厭的事情:它丟失了原來的棧,這是咱們 在編程語言中一般認爲理所固然的事情。編寫代碼丟失了對棧的掌控,就像駕駛一輛沒有剎車的汽車那樣,你不知道它會駛向哪裏。

Promise的重點是,把異步所丟失的return, throw, 和棧還給咱們。可是你必須知道如何正確使用promises才能利用它們。

新手的錯誤

有的人試圖將Promise解釋爲卡通,或者這樣形容:「哦,這個返回值就是異步回來的結果」

我以爲這種解釋不是頗有幫助。對我來講,Promises都是關於代碼結構和流程的。因此我認爲,最好回顧一些常見的錯誤而且想一想怎麼修復它們。我稱之爲「菜鳥錯誤」的意思是:「你如今是一個菜鳥,但你很快會成爲一個職業選手」

Promise對不少人來講意味着不一樣的東西,但就本文而言,我只談論官方規範,在現代瀏覽器中暴露爲window.Promise

新手錯誤1: 厄運金字塔

基於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執行完後面一個纔會執行,並將前一個的返回值做爲參數。稍後會詳細介紹。

新手錯誤2: 怎麼把forEach和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也會被拒絕,這更有用。

新手錯誤3: 忘記catch()

這也是一個常見的錯誤。自信地認爲他們的代碼不會發生異常。不幸的是,這意味着任何的錯誤都會被吞下,你甚至都不會在控制檯中看到它們,這纔是最痛苦的。

爲了不這種狀況,我已經習慣在promise鏈中添加這樣的代碼:

somePromise().then(function () {
  return anotherPromise();
}).then(function () {
  return yetAnotherPromise();
}).catch(console.log.bind(console)); // <-- this is badass
複製代碼

即便你很是肯定不會發生任何錯誤,最好仍是添加一個catch(),讓生活更美好。

新手錯誤4: 使用「deferred」

【辣椒沒看懂這段。大概是,用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, 我就是看不慣這種寫法。】

新手錯誤5:「using side effects instead of returning」

下面這段代碼有什麼問題?

somePromise().then(function () {
  someOtherPromise();
}).then(function () {
  // 哎呀,我但願someOtherPromise()已經resolved了!
  // 劇透:並無
});
複製代碼

正如我以前所說,Promise的魔力在於它們將咱們的return和throw帶回來。 但這在實踐中其實是什麼樣的?

每一個promise都會給你一個then()方法(或者catch(),是語法糖,能夠在then的第二個參數處理錯誤then(null,...))。 這裏咱們在then()函數內:

somePromise().then(function () {
  // 我在then裏面
});
複製代碼

咱們在這兒能夠作三件事:

  1. 返回另外一個promise

  2. 返回一個同步值(或者是undefined)

  3. 拋出一個同步異常

一旦你理解了這個技巧,你就理解了Promise。 下面咱們具體說說這三點:

1. 返回另外一個promise

getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // 我拿到了一個用戶帳號!
});
複製代碼

請注意,我正在返回第二個promise。 return相當重要!! 若是我沒有寫return,那麼getUserAccountById()其實是effect,而下一個函數將接收undefined而不是userAccount。

2.返回一個同步值(或者是undefined)

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,建議你也這樣作。

3.拋出一個同步異常

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的程序員犯的錯誤。 可是,若是咱們但願可以解決我在本文開頭提出的問題,咱們還須要繼續討論一下。

高級錯誤1:不知道Promise.resolve()

上面我已經講過,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'));
複製代碼

高級錯誤2:then(resolveHandler).catch(rejectHandler) 和 then(resolveHandler, rejectHandler)都沒有定義。

我上面說過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項目有一些示例測試能夠幫助你入門。

高級錯誤3:Promise與Promise工廠

假設你想按順序依次執行一系列的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啓蒙。

高級錯誤4:好的,若是我想要兩個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的所有。

高級錯誤5: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的知識,咱們應該可以解決我在本文開頭提出的難題。【就是推特那個】

難題1

doSomething().then(function () {
  return doSomethingElse();
}).then(finalHandler);
複製代碼

答案:

doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                                     finalHandler(resultOfDoSomethingElse)
                                     |------------------|
複製代碼

難題2

doSomething().then(function () {
  doSomethingElse();
}).then(finalHandler);
複製代碼

答案:

doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                  finalHandler(undefined)
                  |------------------|
複製代碼

難題3

doSomething().then(doSomethingElse())
  .then(finalHandler);
複製代碼

答案:

doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
                  finalHandler(resultOfDoSomething)
                  |------------------|
複製代碼

難題4

doSomething().then(doSomethingElse)
  .then(finalHandler);
複製代碼

答案

doSomething
|-----------------|
                  doSomethingElse(resultOfDoSomething)
                  |------------------|
                                     finalHandler(resultOfDoSomethingElse)
                                     |------------------|
複製代碼

若是這些答案仍然沒有起到做用,那麼我建議你從新閱讀帖子,或者定義doSomething()和doSomethingElse()方法,並在瀏覽器中自行嘗試。

【文章年代久遠,那時候es7還沒出,可是依然有些參考意義。如今異步編程的解決辦法,大可能是Promise+async/await, 即:用Promise封裝異步api(好比fs.readFile),在外部的async方法中await剛纔封裝好的方法,爽爽的!這將從新審視這篇文章的做者提出的一些異步寫法】

相關文章
相關標籤/搜索