【譯】關於 Promise 的 9 個提示

正如同事所說的那樣,Promise 在工做中表現優異。javascript

prom

這篇文章會給你一些如何改善與 Promise 之間關係的建議。html

1. 你能夠在 .then 裏面 return 一個 Promise

讓我來講明這最重要的一點前端

是的!你能夠在 .then 裏面 return 一個 Promisejava

並且,return 的這個 Promise 將在下一個 .then 中自動解析。node

.then(r => {
    return serverStatusPromise(r); // 返回 { statusCode: 200 } 的 Promise
})
.then(resp => {
    console.log(resp.statusCode); // 200; 注意自動解析的 promise
})
複製代碼

2. 每次執行 .then 的時候都會自動建立一個新的 Promise

若是熟悉 javascript 的鏈式風格,那麼你應該會感到很熟悉。可是對於一個初學者來講,可能就不會了。android

在 Promise 中不論你使用 .then 或者 .catch 都會建立一個新的 Promise。這個 Promise 是剛剛鏈式調用的 Promise 和 剛剛加上的 .then / .catch 的組合。ios

讓咱們來看一個 🌰:git

var statusProm = fetchServerStatus();

var promA = statusProm.then(r => (r.statusCode === 200 ? "good" : "bad"));

var promB = promA.then(r => (r === "good" ? "ALL OK" : "NOTOK"));

var promC = statusProm.then(r => fetchThisAnotherThing());
複製代碼

上面 Promise 的關係能夠在流程圖中清晰的描述出來: github

image

須要特別注意的是 promApromBpromC 所有都是不一樣的可是有關聯的 Promise。算法

我喜歡把 .then 想像成一個大型管道,當上遊節點出現問題時,水就會中止流向下游。例如,若是 promB 失敗,下游節點不會受到影響,可是若是 statusProm 失敗,那麼下游的全部節點都將受到影響,即 rejected

3. 對調用者來講,Promiseresolved/rejected 狀態是惟一的

我認爲這個是讓 Promise 好好運行的最重要的事情之一。簡單來講,若是在你的應用中 Promise 在不少不一樣的模塊之間共享,那麼當 Promise 返回 resolved/rejected 狀態時,全部的調用者都會收到通知。

這也意味着沒有人能夠改變你的 Promise,因此能夠放心的把它傳遞出去。

function yourFunc() {
  const yourAwesomeProm = makeMeProm();

  yourEvilUncle(yourAwesomeProm); // 不管 Promise 受到了怎樣的影響,它最終都會成功執行

  return yourAwesomeProm.then(r => importantProcessing(r));
}

function yourEvilUncle(prom) {
  return prom.then(r => Promise.reject("destroy!!")); // 可能遭受的影響
}
複製代碼

經過上面的例子能夠看出,Promise 的設計使得自身很難被改變。正如我上面所說的:"保持冷靜,並將 Promise 傳遞下去"。

4. Promise 構造函數不是解決方案

我看到不少開發者喜歡用構造函數的風格,他們認爲這就是 Promise 的方式。但這倒是一個謊話,實際的緣由是構造函數 API 和以前回調函數的 API 類似,並且這樣的習慣很難改變。

若是你發現本身正在處處使用 Promise 構造函數,那你的作法是錯的!

要真正的向前邁進一步而且擺脫回調,你須要當心謹慎而且最小程度地使用 Promise 構造函數。

讓咱們看一下使用 Promise 構造函數 的具體狀況:

return new Promise((res, rej) => {
  fs.readFile("/etc/passwd", function(err, data) {
    if (err) return rej(err);
    return res(data);
  });
});
複製代碼

Promise 構造函數 應該只在你想要把回調轉換成 Promise 時使用。 一旦你掌握了這種建立 Promise 的優雅方式,它將會變的很是有吸引力。

讓咱們看一下冗餘的 Promise 構造函數

☠️錯誤的

return new Promise((res, rej) => {
    var fetchPromise = fetchSomeData(.....);
    fetchPromise
        .then(data => {
            res(data); // 錯誤!!!
        })
        .catch(err => rej(err))
})
複製代碼

💖正確的

return fetchSomeData(...); // 正確的!
複製代碼

Promise 構造函數 封裝 Promise 是多餘的,而且違背了 Promise 自己的目的

😎高級技巧

若是你是一個 nodejs 開發者,我建議你能夠看一看 util.promisify。這個方法能夠幫助你把 node 風格的回調轉換爲 Promise。

const {promisify} = require('util');
const fs = require('fs');

const readFileAsync = promisify(fs.readFile);

readFileAsync('myfile.txt', 'utf-8')
  .then(r => console.log(r))
  .catch(e => console.error(e));
複製代碼

5. 使用 Promise.resolve

Javascript 提供了 Promise.resolve 方法,像下面的例子這樣簡潔:

var similarProm = new Promise(res => res(5));
// ^^ 等價於
var prom = Promise.resolve(5);
複製代碼

它有多種使用狀況,我最喜歡的一種是能夠把普通的(異步的)js 對象轉化成 Promise。

// 將同步函數轉換爲異步函數
function foo() {
  return Promise.resolve(5);
}
複製代碼

當不肯定它是一個 Promise 仍是一個普通的值的時候,你也能夠作一個安全的封裝。

function goodProm(maybePromise) {
  return Promise.resolve(maybePromise);
}

goodProm(5).then(console.log); // 5

var sixPromise = fetchMeNumber(6);

goodProm(sixPromise).then(console.log); // 6

goodProm(Promise.resolve(Promise.resolve(5))).then(console.log); // 5, 注意,它會自動解析全部的 Promise!
複製代碼

6.使用 Promise.reject

Javascript 也提供了 Promise.reject 方法。像下面的例子這樣簡潔:

var rejProm = new Promise((res, reject) => reject(5));

rejProm.catch(e => console.log(e)) // 5
複製代碼

我最喜歡的用法是提早使用 Promise.reject 來拒絕。

function foo(myVal) {
    if (!mVal) {
        return Promise.reject(new Error('myVal is required'))
    }
    return new Promise((res, rej) => {
        // 從你的大回調到 Promise 的轉換!
    })
}
複製代碼

簡單來講,使用 Promise.reject 能夠拒絕任何你想要拒絕的 Promise。

在下面的例子中,我在 .then 裏面使用:

.then(val => {
  if (val != 5) {
    return Promise.reject('Not Good');
  }
})
.catch(e => console.log(e)) // 這樣是很差的
複製代碼

注意:你能夠像 Promise.resolve 同樣在 Promise.reject 中傳遞任何值。你常常在失敗的 Promise 中發現 Error 的緣由是由於它主要就是用來拋出一個異步錯誤的。

7. 使用 Promise.all

Javascript 提供了 Promise.all 方法。像 ... 這樣的簡潔,好吧,我想不出來例子了😁。

在僞算法中,Promise.all 能夠被歸納爲:

接收一個 Promise 數組

    而後同時運行他們

    而後等到他們所有運行完成

    而後 return 一個新的 Promise 數組

    他們其中有一個失敗或者 reject,均可以被捕獲。
複製代碼

下面的例子展現了全部的 Promise 完成的狀況:

var prom1 = Promise.resolve(5);
var prom2 = fetchServerStatus(); // 返回 {statusCode: 200} 的 Promise

Proimise.all([prom1, prom2])
.then([val1, val2] => { // 注意,這裏被解析成一個數組
    console.log(val1); // 5
    console.log(val2.statusCode); // 200
})
複製代碼

下面的例子展現了當他們其中一個失敗的狀況:

var prom1 = Promise.reject(5);
var prom2 = fetchServerStatus(); // 返回 {statusCode: 200} 的 Promise

Proimise.all([prom1, prom2])
.then([val1, val2] => {
    console.log(val1); 
    console.log(val2.statusCode); 
})
.catch(e =>  console.log(e)) // 5, 直接跳轉到 .catch
複製代碼

注意:Promise.all 是很聰明的!若是其中一個 Promise 失敗了,它不會等到全部的 Promise 完成,而是當即停止!

8. 不要懼怕 reject,也不要在每一個 .then 後面加冗餘的 .catch

咱們是否是會常常擔憂錯誤會在它們之間的某處被吞噬?

爲了克服這個恐懼,這裏有一個簡單的小提示:

讓 reject 來處理上游函數的問題。

在理想的狀況下,reject 方法應該是應用的根源,全部的 reject 都會向下傳遞。

不要懼怕像下面這樣寫

return fetchSomeData(...);
複製代碼

如今若是你想要處理函數中 reject 的狀況,請決定是解決問題仍是繼續 reject。

💘 解決 reject

解決 reject 是很簡單的,在 .catch 不論你返回什麼內容,都將被假定爲已解決的。然而,若是你在 .catch 中返回 Promise.reject,那麼這個 Promise 將會是失敗的。

.then(() => 5.length) // <-- 這裏會報錯
.catch(e => {
        return 5;  // <-- 從新使方法正常運行
})
.then(r => {
    console.log(r); // 5
})
.catch(e => {
    console.error(e); // 這個方法永遠不會被調用 :)
})
複製代碼

💔拒絕一個 reject

拒絕一個 reject 是簡單的。不須要作任何事情。 就像我剛剛說的,讓它成爲其餘函數的問題。一般狀況下,父函數有比當前函數處理 reject 更好的方法。

須要記住的重要的一點是,一旦你寫了 catch 方法,就意味着你正在處理這個錯誤。這個和同步 try/catch的工做方式類似。

若是你確實想要攔截一個 reject:(我強烈建議不要這樣作!)

.then(() => 5.length) // <-- 這裏會報錯
.catch(e => {
  errorLogger(e); // 作一些錯誤處理
  return Promise.reject(e); // 拒絕它,是的,你能夠這麼作!
})
.then(r => {
    console.log(r); // 這個 .then (或者任何後面的 .then) 將永遠不會被調用,由於咱們在上面使用了 reject :)
})
.catch(e => {
    console.error(e); //<-- 它變成了這個 catch 方法的問題
})
複製代碼

.then(x,y) 和 then(x).catch(x) 之間的分界線

.then 接收的第二個回調函數參數也能夠用來處理錯誤。它和 then(x).catch(x) 看起來很像,可是他們處理錯誤的區別在於他們自身捕獲的錯誤。

我會用下面的例子來講明這一點:

.then(function() {
   return Promise.reject(new Error('something wrong happened'));
}).catch(function(e) {
   console.error(e); // something wrong happened
});

.then(function() {
   return Promise.reject(new Error('something wrong happened'));
}, function(e) { // 這個回調處理來自當前 `.then` 方法以前的錯誤
    console.error(e); // 沒有錯誤被打印出來
});
複製代碼

當你想要處理的是來自上游 Promise 而不是剛剛在 .then 裏面加上去的錯誤的時候, .then(x,y) 變的很方便。

提示: 99.9% 的狀況使用簡單的 then(x).catch(x) 更好。

9. 避免 .then 回調地獄

這個提示是相對簡單的,儘可能避免 .then 裏包含 .then 或者 .catch。相信我,這比你想象的更容易避免。

☠️錯誤的

request(opts)
.catch(err => {
  if (err.statusCode === 400) {
    return request(opts)
           .then(r => r.text())
           .catch(err2 => console.error(err2))
  }
})
複製代碼

💖正確的

request(opts)
.catch(err => {
  if (err.statusCode === 400) {
    return request(opts);
  }
})
.then(r => r.text())
.catch(err => console.erro(err));
複製代碼

有些時候咱們在 .then 裏面須要不少變量,那就別無選擇了,只能再建立一個 .then 方法鏈。

.then(myVal => {
    const promA = foo(myVal);
    const promB = anotherPromMake(myVal);
    return promA
          .then(valA => {
              return promB.then(valB => hungryFunc(valA, valB)); // 很醜陋!
          })
})
複製代碼

我推薦使用 ES6 的解構方法混合着 Promise.all 方法就能夠解決這個問題。

.then(myVal => {
    const promA = foo(myVal);
    const promB = anotherPromMake(myVal);
    return Promise.all([prom, anotherProm])
})
.then(([valA, valB]) => {   // 很好的使用 ES6 解構
    console.log(valA, valB) // 全部解析後的值
    return hungryFunc(valA, valB)
})
複製代碼

注意:若是你的 node/瀏覽器/老闆/意識容許,還可使用 async/await 方法來解決這個問題。

我真心但願這篇文章對你理解 Promise 有所幫助。

請查看我以前的博客文章。

若是你 ❤️ 這篇文章,請分享這篇文章來傳播它。

在 Twitter 上聯繫我 @kushan2020


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索