在Node.js 中用 Q 實現Promise – Callbacks之外的另一種選擇

海興發表於2013-08-31

原文: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函式,分別是onFulfilledonRejected

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

promise被解決時(非同步處理已經完成)會呼叫onFulfilledonRejected 。因為只會有一種結果,所以這兩個函式中僅有一個會被觸發。

從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(onFulfilledonRejected)的普通函式。詳情請見此文

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可用的。請參見 readmeAPI 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,最好的辦法就是用一用。下面是幾個幫你開始的主意:

  1. 封裝一些基本的Node流程,將callbacks 變成 promises
  2. 重寫一個async方法,變成使用promise的
  3. 寫一些遞迴使用promises的東西(目錄樹應該是個不錯的開端)
  4. 寫一個過得去的 Promise A+實現

相關文章