在Node.js 中用 Q 實現Promise – Callbacks之外的另一種選擇
原文:Promises in Node.js with Q – An Alternative to Callbacks
by Marc Harter 《Node.js in Practice》
怎麼寫非同步程式碼?相對原始的callbacks而言,promises無疑是更好的選擇。可掌握promises的概念及其用法可能不太容易, 而且很有可能你已經放棄它了。但經過一大波碼農的努力,promise的美終於以一種可互操、可驗證的方式現於世間。這一努力的結果就是Promises/A+規範,它以自己的方式影響了各種promises庫,甚至DOM。
扯了這麼多,promises到底是什麼?寫Node程式時它能幫上什麼忙?
Promises是一個。。。抽象
我們先來聊聊promise的行為模式,讓你對他是什麼,能怎麼用他有個直觀的感受。在本文的後半段,我們會以Q為例講一下在程式裡怎麼建立和使用promise。
那promise究竟是什麼呢?請看定義:
promise是對非同步程式設計的一種抽象。它是一個代理物件,代表一個必須進行非同步處理的函式返回的值或丟擲的異常。 – Kris Kowal on JSJ
callback是編寫Javascript非同步程式碼最最最簡單的機制。可用這種原始的callback必須以犧牲控制流、異常處理和函式語義為代價,而我們在同步程式碼中已經習慣了它們的存在,不適應!Promises能帶它們回來。
promise物件的核心部件是它的then
方法。我們可以用這個方法從非同步操作中得到返回值(傳說中的履約值),或丟擲的異常(傳說中的拒絕的理由)。then
方法有兩個可選的引數,都是callback函式,分別是onFulfilled
和 onRejected
:
var promise = doSomethingAync()
promise.then(onFulfilled, onRejected)
promise被解決時(非同步處理已經完成)會呼叫onFulfilled
和 onRejected
。因為只會有一種結果,所以這兩個函式中僅有一個會被觸發。
從Callbacks 到 promises
看過這個promises的基礎知識後,我們再來看一個經典的非同步 Node callback:
readFile(function (err, data) {
if (err) return console.error(err)
console.log(data)
})
如果函式readFile
返回的是 promise,我們可以這樣寫:
var promise = readFile()
promise.then(console.log, console.error)
乍一看這沒什麼實質性變化。但實際上現在我們得到了一個代表非同步操作的值(promise)。我們可以傳遞promise,不管非同步操作完成與否,所有能訪問到promise的程式碼都可以用then
使用這個非同步操作的處理結果。而且我們還得到保證,非同步操作的結果不會發生某種變化,因為promise只會被解決一次(或履約,或被拒)。
把
then
當成對promise解包以得到非同步操作結果(或異常)的函式對理解promise更有幫助,不要把它當成只是帶兩個callback(onFulfilled
和onRejected
)的普通函式。詳情請見此文
promise的連結及內嵌
then
方法返回的還是promise。
var promise = readFile()
var promise2 = promise.then(readAnotherFile, console.error)
這個promise表示 onFulfilled 或 onRejected 的返回結果。既然結果只能是其中之一,所以不管是什麼結果,promise都會轉發呼叫:
var promise = readFile()
var 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可以形成呼叫鏈,避免出現callback大坑:
readFile()
.then(readAnotherFile)
.then(doSomethingElse)
.then(...)
如果非要保持閉包,promise也可以巢狀:
readFile()
.then(function (data) {
return readAnotherFile().then(function () {
// do something with `data`
})
})
Promise與同步函式
Promises有幾種編寫同步函式的辦法。其中之一是用返回代替呼叫。在前面的例子中,返回readAnotherFile()
是一個訊號,表明在readFile
完成之後做什麼。
如果返回promise,它會在非同步操作完成後發訊號給下一個then
。返回值並不是非promise不可,不管返回什麼,都會傳給下一個onFulfilled
做引數:
readFile()
.then(function (buf) {
return JSON.parse(buf.toString())
})
.then(function (data) {
// do something with `data`
})
promise的錯誤處理
除了return
,還可以用關鍵字throw
和 try/catch語法。這可以算是promises最強的一個特性了。下面我們來看一段同步程式碼:
try {
doThis()
doThat()
} catch (err) {
console.error(err)
}
在上例中,如果doThis()
或 doThat()
丟擲了異常,異常會被捕獲並輸出錯誤日誌。既然try/catch允許多個操作放到一起,我們就不用單獨處理每個操作可能出現的錯誤。用promises的非同步程式碼也可以這樣:
doThisAsync()
.then(doThatAsync)
.then(null, console.error)
如果doThisAsync()
沒有成功,它的promise會被拒,處理鏈下一個then
上的onRejected
會被呼叫。在上例中就是函式console.error。而且跟 try/catch 一樣, doThatAsync() 根本就不會被呼叫。對於原始的callback那種每一步裡都要顯式處理錯誤的方式而言,這是巨大的進步。
實際上它比這還要好!任何被丟擲的異常,隱式的或顯式的,then
的回撥函式中的也會處理:
doThisAsync()
.then(function (data) {
data.foo.baz = 'bar' // throws a ReferenceError as foo is not defined
})
.then(null, console.error)
上例中丟擲的ReferenceError
會被處理鏈中下一個onRejected
捕獲。相當漂亮!當然,這對顯式丟擲的異常也有效:
doThisAsync()
.then(function (data) {
if (!data.baz) throw new Error('Expected baz to be there')
})
.then(null, console.error)
對錯誤處理的重要提示
我們在前面已經說過了,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')
})
要處理被遮蔽的錯誤,可以在promise處理鏈的最後加一個.then(null, onRejected)
:
readFile()
.then(function (data) {
throw new Error('now I know this happened')
})
.then(null, console.error)
各種函式庫中還包括暴露被遮蔽錯誤的其他選項。比如Q中的
done
方法可以重新向上丟擲錯誤。
promise的具體應用
前面的例子都是返回空方法,只是為了闡明Promises/A+中的then
方法。接下來我們要看一些更具體的例子。
將callbacks 變成 promises
你可能在想promise最初是從哪蹦出來的。Promise/A+規範中沒有規定建立promise的API,因為它不會影響互操作性。因此不同promise庫的實現可能是不同的。我們的例子用的是Q(npm install q
).
Node 核心非同步函式不會返回promises;它們採用了callbacks的方式。然而用Q可以很容易地讓它們返回promises:
var fs_readFile = Q.denodify(fs.readFile)
var promise = fs_readFile('myfile.txt')
promise.then(console.log, console.error)
Q 提供了一些輔助函式,可以將Node和其他環境適配為promise可用的。請參見 readme 和 API documentation 瞭解詳情。
建立原始的promise
用Q.defer
可以手動建立promise。比如將fs.readFile
手工封裝成promise的(基本上就是 Q.denodify
做的事情 )
function fs_readFile (file, encoding) {
var deferred = Q.defer()
fs.readFile(file, encoding, function (err, data) {
if (err) deferred.reject(err) // rejects the promise with `er` as the reason
else deferred.resolve(data) // fulfills the promise with `data` as the value
})
return deferred.promise // the promise is returned
}
fs_readFile('myfile.txt').then(console.log, console.error)
做同時支援callbacks 和 promises 的APIs
我們已經見過兩種將callback程式碼變成promise程式碼的辦法了。其實還能做出同時提供promise和callback介面的APIs。下面我們就把fs.readFile
變成這樣的API:
function fs_readFile (file, encoding, callback) {
var deferred = Q.defer()
fs.readFile(function (err, data) {
if (err) deferred.reject(err) // rejects the promise with `er` as the reason
else deferred.resolve(data) // fulfills the promise with `data` as the value
})
return deferred.promise.nodeify(callback) // the promise is returned
}
如果提供了callback,當promise被拒或被解決時,會用標準Node風格的(err, result) 引數呼叫它。
fs_readFile('myfile.txt', 'utf8', function (er, data) {
// ...
})
用promise執行並行操作
我們前面聊的都是順序的非同步操作。對於並行操作,Q提供了Q.all
方法,它以一個promises陣列作為輸入,返回一個新的promise。 在陣列中的所有操作都成功完成後,這個promise就會履約。如果任何一個操作失敗,這個新的promise就會被拒。
var allPromise = Q.all([ fs_readFile('file1.txt'), fs_readFile('file2.txt') ])
allPromise.then(console.log, console.error)
不得不強調一下,promise在模仿函式。函式只有一個返回值。當傳給
Q.all
兩個成功完成的promises時,呼叫onFulfilled
只會有一個引數(一個包含兩個結果的陣列)。你可能會對此感到吃驚;然而跟同步保持一致是promise的一個重要保證。如果你想把結果展開成多個引數,可以用Q.spread
。
讓promise更具體
要想真正理解promise,最好的辦法就是用一用。下面是幾個幫你開始的主意:
- 封裝一些基本的Node流程,將callbacks 變成 promises
- 重寫一個async方法,變成使用promise的
- 寫一些遞迴使用promises的東西(目錄樹應該是個不錯的開端)
- 寫一個過得去的 Promise A+實現
相關文章
- 除gRPC之外的另一個選擇,IceRPC-支援QUICRPCUI
- 選擇排序中用異或實現swap()時出現的問題排序
- Swagger之外的選擇Swagger
- 《大教堂與集市》:軟體工程的另一種選擇軟體工程
- .Net 8.0 除gRPC之外的另一個選擇,IceRPC之快速開始HelloWorldRPC
- HTM – JSX 的替代品?還是另一種選擇?JS
- HTM - JSX 的替代品?還是另一種選擇?JS
- Java之外選擇Scala還是Groovy?Java
- Camunda JavaDelegate另一種實現Java
- 無棧創業:全棧創業之外的另一種嘗試創業全棧
- 前端另一種多語言的實現思路前端
- DCI中場景的另一種實現和思考
- win與linux間的通訊除Samba之外的另一種方案(轉)LinuxSamba
- 選擇排序java實現排序Java
- 在VB中用API實現多媒體 (轉)API
- cache2k:Guava Cache及Caffeine之外的新選擇Guava
- NVM、NPM、Node.js的安裝選擇NPMNode.js
- vue實現城市列表選擇Vue
- 選擇排序(python)實現排序Python
- 使用 CSS 選擇器實現對不含 title 屬性元素的選擇CSS
- 商品屬性的選擇功能的實現
- 在PHP中用協同程式實現合作多工PHP
- Java反編譯器JAD等的另一種選擇JD-Core/JD-GUIJava編譯GUI
- 實現自己的promisePromise
- Promise 方法的實現Promise
- Promise實現Promise
- Nginx的另一個選擇 - Traefik 入手及簡單配置Nginx
- [原] 探索 EventEmitter 在 Node.js 中的實現MITNode.js
- 在 Node.js 中使用 Promise.prototype.finallyNode.jsPromise
- [Ruby Summit 2018 話題分享] 模組化的 Rails,微服務以外的另一種選擇MITAI微服務
- jquery實現上下滑動選擇jQuery
- 【springboot】介面多實現類,選擇性注入的4種解決方案Spring Boot
- [Java實現] 圖片擇優(選擇最清楚的圖片)Java
- AbortSignal:以前我沒得選,現在我想中止promisePromise
- 31種選擇器的應用
- 選擇下拉選單項實現跳轉效果
- Promise的實現及解析Promise
- promise的模擬實現Promise