在 Node.js 中將回調轉換為 Promise
簡介
幾年前,回調是我們在 JavaScript 中實現異步代碼執行的唯一方法。回調的問題很少,最引人注目的是“回調地獄”。
在 ES6 中,承諾 被引入作為這些問題的解決方案。最後,async/await
引入關鍵字是為了獲得更愉快的體驗並提高可讀性。
即使添加了新方法,仍然有很多本地模塊和庫使用回調。在本文中,我們將討論如何將 JavaScript 回調轉換為 Promise。 ES6 的知識會派上用場,因為我們將使用諸如擴展運算符之類的功能來使事情變得更容易。
什麼是回調
回調是一個函數參數,它恰好是一個函數本身。雖然我們可以創建任何函數來接受另一個函數,但回調主要用於異步操作。
JavaScript 是一種解釋型語言,一次只能處理一行代碼。有些任務可能需要很長時間才能完成,例如下載或讀取大文件。 JavaScript 將這些長時間運行的任務卸載到瀏覽器或 Node.js 環境中的不同進程。這樣,它不會阻止所有其他代碼被執行。
通常,異步函數接受一個回調函數,以便在它們完成後我們可以處理它們的數據。
舉個例子,我們寫一個回調函數,當程序成功從我們的硬盤讀取一個文件時,就會執行這個回調函數。
為此,我們將使用一個名為 sample.txt
的文本文件 ,包含以下內容:
Hello world from sample.txt
那我們來寫一個簡單的 Node.js 腳本來讀取文件:
const fs = require('fs');
fs.readFile('./sample.txt', 'utf-8', (err, data) => {
if (err) {
// Handle error
console.error(err);
return;
}
// Data is string do something with it
console.log(data);
});
for (let i = 0; i < 10; i++) {
console.log(i);
}
運行這段代碼應該會產生:
0
...
8
9
Hello world from sample.txt
如果您運行此代碼,您應該會看到 0..9
在回調執行之前打印。這是因為我們之前談到的 JavaScript 的異步管理。記錄文件內容的回調,只有在文件被讀取後才會被調用。
附帶說明一下,回調也可以在同步方法中使用。例如,Array.sort()
接受一個回調函數,允許您自定義元素的排序方式。
現在我們對回調有了更好的了解。讓我們繼續看看 Promise 是什麼。
什麼是承諾
ECMAScript 2015 引入了 Promise (俗稱ES6 ) 以改善異步編程的開發人員體驗。顧名思義,這是一個承諾 JavaScript 對象最終會返回一個 value 或錯誤 .
一個 Promise 有 3 個狀態:
- 待處理 :表示異步操作未完成的初始狀態。
- 已完成 :表示異步操作成功完成。
- 拒絕 :表示異步操作失敗。
大多數承諾最終看起來像這樣:
someAsynchronousFunction()
.then(data => {
// After promise is fulfilled
console.log(data);
})
.catch(err => {
// If promise is rejected
console.error(err);
});
Promise 在現代 JavaScript 中很重要,因為它們與 async/await
一起使用 ECMAScript 2016 中引入的關鍵字 .使用 async/await
,我們不需要使用回調或 then()
和 catch()
寫異步代碼。
如果要修改前面的示例,它將如下所示:
try {
const data = await someAsynchronousFunction();
} catch(err) {
// If promise is rejected
console.error(err);
}
這看起來很像“常規”同步 JavaScript!您可以了解更多關於 async/await
在我們的文章中,ES7 中的 Node.js Async Await。
大多數流行的 JavaScript 庫和新項目使用帶有 async/await
的 Promises 關鍵字。
但是,如果您正在更新現有的 repo 或遇到遺留代碼庫,您可能會對將基於回調的 API 移動到基於 Promise 的 API 以改善您的開發體驗感興趣。您的團隊也會感激不盡。
讓我們看幾個將回調轉換為 Promise 的方法!
將回調轉換為承諾
Node.js Promisify
Node.js 中大部分接受回調的異步函數,例如 fs
(文件系統)模塊,具有標準的實現方式 - 回調作為最後一個參數傳遞。
例如,這裡是如何使用 fs.readFile()
讀取文件 不指定文本編碼:
fs.readFile('./sample.txt', (err, data) => {
if (err) {
console.error(err);
return;
}
// Data is a buffer
console.log(data);
});
免費電子書:Git Essentials
查看我們的 Git 學習實踐指南,其中包含最佳實踐、行業認可的標準以及隨附的備忘單。停止谷歌搜索 Git 命令並真正學習 它!
注意 :如果你指定 utf-8
作為編碼,您將獲得字符串輸出。如果你不指定編碼,你會得到一個 Buffer
輸出。
此外,傳遞給函數的回調應該接受 Error
因為它是第一個參數。之後,可以有任意數量的輸出。
如果您需要轉換為 Promise 的函數遵循這些規則,那麼您可以使用 util.promisify,這是一個將回調轉換為 Promise 的原生 Node.js 模塊。
為此,首先導入 util
模塊:
const util = require('util');
然後你使用 promisify
將其轉換為承諾的方法:
const fs = require('fs');
const readFile = util.promisify(fs.readFile);
現在將新創建的函數用作常規承諾:
readFile('./sample.txt', 'utf-8')
.then(data => {
console.log(data);
})
.catch(err => {
console.log(err);
});
或者,您可以使用 async/await
關鍵字如下例所示:
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
(async () => {
try {
const content = await readFile('./sample.txt', 'utf-8');
console.log(content);
} catch (err) {
console.error(err);
}
})();
您只能使用 await
使用 async
創建的函數中的關鍵字 ,因此我們在這個例子中有一個函數包裝器。此函數包裝器也稱為立即調用函數表達式。
如果您的回調不遵循該特定標準,請不要擔心。 util.promisify()
函數可以讓您自定義轉換的發生方式。
注意 :Promises 在引入後很快就流行起來。 Node.js 已經將其大部分(如果不是全部)核心函數從回調轉換為基於 Promise 的 API。
如果您需要使用 Promises 處理文件,請使用 Node.js 附帶的庫。
到目前為止,您已經學習瞭如何將 Node.js 標準樣式的回調轉換為 Promise。此模塊僅在 Node.js 版本 8 及以後可用。如果您在瀏覽器或早期版本的 Node 中工作,最好創建自己的基於 Promise 的函數版本。
創建你的承諾
讓我們談談如果 util.promisify()
功能不可用。
這個想法是創建一個新的 Promise
環繞回調函數的對象。如果回調函數返回錯誤,我們會拒絕帶有錯誤的 Promise。如果回調函數返回非錯誤輸出,我們用輸出解析 Promise。
讓我們首先將回調轉換為接受固定數量參數的函數的承諾:
const fs = require('fs');
const readFile = (fileName, encoding) => {
return new Promise((resolve, reject) => {
fs.readFile(fileName, encoding, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
}
readFile('./sample.txt')
.then(data => {
console.log(data);
})
.catch(err => {
console.log(err);
});
我們的新函數 readFile()
接受我們用來讀取文件的兩個參數 fs.readFile()
.然後我們創建一個新的 Promise
包裹函數的對象,該函數接受回調,在本例中為 fs.readFile()
.
我們沒有返回錯誤,而是 reject
承諾。我們不是立即記錄數據,而是 resolve
承諾。然後我們使用基於 Promise 的 readFile()
功能和以前一樣。
讓我們嘗試另一個接受動態數量參數的函數:
const getMaxCustom = (callback, ...args) => {
let max = -Infinity;
for (let i of args) {
if (i > max) {
max = i;
}
}
callback(max);
}
getMaxCustom((max) => { console.log('Max is ' + max) }, 10, 2, 23, 1, 111, 20);
callback參數也是第一個參數,對於接受回調的函數來說有點不尋常。
轉換為 Promise 的方式相同。我們新建一個Promise
包裹我們使用回調的函數的對象。然後我們 reject
如果我們遇到錯誤和 resolve
當我們得到結果時。
我們承諾的版本如下所示:
const getMaxPromise = (...args) => {
return new Promise((resolve) => {
getMaxCustom((max) => {
resolve(max);
}, ...args);
});
}
getMaxCustom(10, 2, 23, 1, 111, 20)
.then(max => console.log(max));
在創建我們的 Promise 時,函數是否以非標準方式使用回調或帶有許多參數並不重要。我們可以完全控制它是如何完成的,而且原理是一樣的。
結論
雖然回調是在 JavaScript 中利用異步代碼的默認方式,但 Promise 是一種更現代的方法,開發人員認為它更易於使用。如果我們遇到使用回調的代碼庫,我們現在可以將該函數設為 Promise。
在本文中,您首先看到瞭如何使用 utils.promisfy()
Node.js 中的方法將接受回調的函數轉換為 Promise。然後您看到瞭如何創建自己的 Promise
包裝一個函數的對象,該函數在不使用外部庫的情況下接受回調。
有了這個,許多遺留的 JavaScript 代碼可以很容易地與更現代的代碼庫和實踐混合在一起!與往常一樣,源代碼在 GitHub 上可用。