我們所瞭解的Promise

Node.js JavaScript 泛函編程 碼坊 2019-07-02

瞭解為什麼以及如何在Node.js中使用promises

我們所瞭解的Promise

回調是在JavaScript中處理異步代碼的最簡單的機制。 然而,原始回調犧牲了開發人員在同步代碼中熟悉的控制流,異常處理和函數語義:

// Asynchronous operations return no meaningful value
var noValue = fs.readFile('file1.txt', function(err, buf) {
// Errors are explicitly handled every time
if (err) return handleErr(err)
fs.readFile('file2.txt', function(err2, buf2) {
if (err2) return handleErr(err2)
data.foo.baz = 'bar' // Exceptions like this ReferenceError are not caught
// Sequential operations encourage heavy nesting
fs.readFile('file3.txt', function(err3, buf3) {
if (err3) return handleErr(err3)
})
})
})

Promise提供了一種方法來恢復控制:

更強大的控制流程

更好的異常處理

函數式編程語義

儘管如此,Promise可能會令人困惑,因此你可能已經將它們寫下來或直接跳過async / await,這為JavaScript提供了新的Promise語法。

但是,瞭解Promise如何在基本層面上發揮作用和行為將有助於您充分利用它們。 在本文中,我們將介紹Promise的基礎知識,包括它是什麼,如何創建它,以及如何最有效地使用它。

抽像的Promise

首先,讓我們來看看Promise的行為:它是什麼以及它如何有用? 然後我們將討論如何創建和使用Promise。

什麼是Promise? 我們來看一個定義:

Promise是異步編程的抽象。 它是一個對象,代理返回值或由必須進行異步處理的函數拋出的異常。 - JSJ的Kris Kowal

Promise對象的核心組件是它的then方法。 then方法是如何從異步操作中獲取返回值(稱為實現值)或拋出異常(稱為拒絕原因)。 然後將兩個可選的回調作為參數,我們將調用onFulfilled和onRejected:

let promise = doSomethingAync()
promise.then(onFulfilled, onRejected)

當Promise解析時(異步處理已完成),onFulfilled和onRejected觸發器。 其中一個功能將觸發,因為只有一個是可能的。

Promise回調

鑑於這些Promise的基本知識,讓我們看一下熟悉的異步Node.js回調:

readFile(function(err, data) => {
if (err) return console.error(err)
console.log(data)
})

如果我們的readFile函數返回了一個promise,我們將編寫相同的邏輯:

let promise = readFile()
promise.then(console.log, consoler.error)

乍一看,它似乎改變了美學。 但是,我們現在可以訪問表示異步操作(promise)的值。 我們可以在代碼中傳遞Promise,就像JavaScript中的任何其他值一樣。 無論異步操作是否已完成,任何有權訪問promise的人都可以使用它。 我們還保證異步操作的結果不會以某種方式改變,因為promise將resolve一次(即fulfilled 或 rejected)。

將其視為一個需要兩個回調函數(onFulfilled和onRejected)的函數是有幫助的,但是作為一個函數,它展開了揭示異步操作發生的情況。 任何有權訪問權限的人都可以使用它來打開它。

Promise的鏈接與嵌套

then方法返回Promise

let promise = readFile()
let promise2 = promise.then(readAnotherFile, console.error)
我們所瞭解的Promise

此promise表示其onFulfilled或onRejected處理程序的返回值(如果已指定),promise將代理觸發處理程序:

let promise = readFile()
let promise2 = promise.then(
function(data) {
return readAnotherFile() // If readFile was successful, let's readAnotherFile
},
function(err) {
console.error(err) // If readFile was unsuccessful, let's log it but still readAnotherFile
return readAnotherFile()
}
)
promise2.then(console.log, console.error) // The result of readAnotherFile

既然then返回一個Promise,這意味著Promise可以鏈接在一起,以避免回調地獄的深層嵌套:

readFile()
.then(readAnotherFile)
.then(doSomethingElse)
.then(...)

Promse 與同步函數

Promise可以模擬同步功能。 使用return來繼續而不是調用另一個函數。 前面的示例返回readAnotherFile()以表示在readFile()之後要執行的操作。

如果您返回一個promise,它將在異步操作完成時發出下一個信號。 您還可以返回任何其他值,下一個onFulfilled將獲取該值作為參數:

readFile()
.then(function (buf) {
return JSON.parse(buf.toString())
})
.then(function (data) => {
// Do something with `data`
})

Promise的異常處理

您還可以使用throw關鍵字以及try / catch。 這可能是Promise最強大的功能之一。 例如,請考慮以下同步代碼:

try {
doThis()
doThat()
} catch (err) {
console.error(err)
}

在這個例子中,如果doThis()或doThat()會拋出錯誤,我們將捕獲並記錄錯誤。 由於try / catch塊允許分組操作,因此我們可以避免顯式處理每個操作的錯誤。 我們可以使用promises異步執行同樣的操作:

doThisAsync()
.then(doThatAsync)
.then(undefined, console.error)

如果doThisAsync()不成功,則其promise將拒絕,然後在鏈中使用onRejected處理程序觸發。 在這種情況下,它是console.error函數。 和try / catch塊一樣,doThatAsync()永遠不會被調用。 這是對原始回調的改進,您必須在每個步驟中明確處理錯誤。

但是,任何拋出的異常,無論是隱式還是顯式的 - 其回調也在promises中處理:

doThisAsync()
.then(function(data) {
data.foo.baz = 'bar' // Throws a ReferenceError as foo is not defined
})
.then(undefined, console.error)

這裡,引發的ReferenceError觸發鏈中的下一個onRejected處理程序。 很簡單! 當然,這也適用於顯式拋出:

doThisAsync()
.then(function(data) {
if (!data.baz) throw new Error('Expected baz to be there')
})
.catch(console.error) // The catch(fn) is shorthand for .then(undefined, fn)

錯誤處理的重要說明

如前所述,promises模仿try / catch語義。 在try / catch塊中,可以通過從不顯式處理它來掩蓋錯誤:

try {
throw new Error('Never will know this happened')
} catch (e) {}

Promise也是一樣

readFile().then(function(data) {
throw new Error('Never will know this happened')
})

要公開掩碼錯誤,解決方案是使用簡單的.catch(onRejected)子句結束promise鏈:

readFile()
.then(function(data) {
throw new Error('Now I know this happened')
})
.catch(console.error)

構造Promise

您也可以使用Promise構造函數創建一個promise。 讓我們轉換相同的fs.readFile方法來返回promises而不使用util.promisify:

const fs = require('fs')
function readFile(file, encoding) {
return new Promise(function(resolve, reject) {
fs.readFile(file, encoding, function(err, data) {
if (err) return reject(err) // Rejects the promise with `err` as the reason
resolve(data) // Fulfills the promise with `data` as the value
})
})
}
let promise = readFile('myfile.txt')
promise.then(console.log, console.error)

創建支持回調和Promise的API

我們已經看到了兩種將回調代碼轉換為promise代碼的方法。 您還可以創建提供promise和回調接口的API。 例如,讓我們將fs.readFile轉換為支持回調和承諾的API:

const fs = require('fs')
function readFile(file, encoding, callback) {
if (callback) return fs.readFile(file, encoding, callback) // Use callback if provided
return new Promise(function(resolve, reject) {
fs.readFile(file, encoding, function(err, data) {
if (err) return reject(err)
resolve(data)
})
})
}

如果存在回調,則使用標準Node樣式(錯誤,結果)參數觸發它。

readFile('myfile.txt', 'utf8', function(er, data) {
// ...
})

使用promises進行並行操作

我們已經討論過順序異步操作。 對於並行操作,ES6 / 2015提供了Promise.all方法,該方法接受一系列promise並返回一個新的promise。 所有操作完成後,新Promise即告完成。 如果任何操作失敗,則返回reject promise。

let allPromise = Promise.all([readFile('file1.txt'), readFile('file2.txt')])
allPromise.then(console.log, console.error)
我們所瞭解的Promise

總結

理解承諾的最好方法是使用它們。 以下是一些讓您入門的建議:

包裝一些標準的Node.js庫函數,將回調轉換為promises。 沒有使用node.promisify實用程序作弊!

使用async / await獲取函數並重寫它而不使用該語法糖。 這意味著您將返回一個promise並使用then方法。

使用promises遞歸寫一些東西(目錄樹將是一個好的開始)。

相關推薦

推薦中...