關於 Promise 的 9 個提示
關於 Promise 的 9 個提示
正如同事所說的那樣,Promise 在工作中表現優異。
這篇文章會給你一些如何改善與 Promise 之間關係的建議。
1. 你可以在 .then 裡面 return 一個 Promise
讓我來說明這最重要的一點
是的!你可以在 .then 裡面 return 一個 Promise
而且,return 的這個 Promise 將在下一個 .then
中自動解析。
.then(r => { return serverStatusPromise(r); // 返回 { statusCode: 200 } 的 Promise }) .then(resp => { console.log(resp.statusCode); // 200; 注意自動解析的 promise })
2. 每次執行 .then 的時候都會自動建立一個新的 Promise
如果熟悉 javascript 的鏈式風格,那麼你應該會感到很熟悉。但是對於一個初學者來說,可能就不會了。
在 Promise 中不論你使用 .then
或者 .catch
都會建立一個新的 Promise。這個 Promise 是剛剛鏈式呼叫的 Promise 和 剛剛加上的 .then
/ .catch
的組合。
讓我們來看一個 :chestnut::
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 的關係可以在流程圖中清晰的描述出來:
需要特別注意的是 promA
、 promB
和 promC
全部都是不同的但是有關聯的 Promise。
我喜歡把 .then
想像成一個大型管道,當上遊節點出現問題時,水就會停止流向下游。例如,如果 promB
失敗,下游節點不會受到影響,但是如果 statusProm
失敗,那麼下游的所有節點都將受到影響,即 rejected
。
3. 對呼叫者來說, Promise
的 resolved/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)) })
:sparkling_heart: 正確的
return fetchSomeData(...); // 正確的!
用 Promise 建構函式
封裝 Promise 是 多餘的,並且違背了 Promise 本身的目的 。
:sunglasses: 高階技巧
如果你是一個 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));
</div>
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 方法。像 ... 這樣的簡潔,好吧,我想不出來例子了:grin:。
在偽演算法中, 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。
:cupid: 解決 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); // 這個方法永遠不會被呼叫 :) })
:broken_heart: 拒絕一個 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)) } })
:sparkling_heart: 正確的
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 有所幫助。