【譯】回調地獄 Callback Hell


翻譯自:http://callbackhell.com/,水平有限,做個人理解之用。

這是一個編寫異步JavaScript程序的指導手冊。

一、什么是回調地獄?

異步的JavaScript程序,或者說使用了回調函數的JavaScript程序,很難地去直觀順暢地閱讀,大量的代碼以這種方式結束:
fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) { 
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})
有沒有看到這些以"})"結尾的金字塔結構?這個形狀為“親切地”稱為回調地獄。

二、什么是callback(回調)?

“callback"僅僅只是一種使用JavaScript函數的一種通用稱呼。在JavaScript語言中,並沒有一種特定的東西稱之為“callback”,這個只是一種方便的稱呼。不同於大部分立即返回結果的函數,這些使用callback的函數需要消耗一些時間才能返回結果。“asynchronous”(異步),或者簡稱為“async”僅僅表示需要花費一些時間,或者是“在未來發生,而不是現在”。通常情況下,callback僅僅用於操作I/O的時候使用到。比如下載、讀寫文件、與數據庫交互等。
 當調用一個普通的函數的時候,你可以這樣使用返回值:
var result = multiplyTwoNumbers(5, 10)
console.log(result)
// 50 gets printed out
然而,異步函數,也就是使用了callback函數的不會立刻返回任何東西。
var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo is 'undefined'!
在這種情況下,下載gif文件會花費相當長的時間,而且你並不希望你的程序在等待下載結束的過程中處於“暫停”(也就是阻塞,block)狀態。
相反,你可以把下載結束后需要執行的操作存放到一個函數中,這個就是callback(回調)!你提供了一個“downloadPhoto”的函數,並且這個函數會在下載完成的時候執行callback(call you back later)函數,並且傳遞photo參數(或者出錯的時候返回一個錯誤信息)。
downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)
function handlePhoto (error, photo) {
  if (error) console.error('Download error!', error)
  else console.log('Download finished', photo)
}
console.log('Download started')
人們在嘗試理解“callback”這個概念的最大困難之處在於,程序運行的過程中,程序中的代碼,是怎么樣按照規則執行的。在這個例子中,有三件主要的代碼段會發生:首先 “handlePhoto”函數被申明,然后是“downloadPhoto”函數會被調用並且把“handlePhoto”函數作為回調函數“callback”參數傳遞進入,最后,打印一句話“Download started”。
要注意以下,“handlePhoto”並沒有立即被調用,只是創建並且作為一個參數傳遞給了“downloadPhoto”。但是,一旦“downloadPhoto”函數執行完成之后,”handlePhoto“就會運行。這個取決於網絡連接到底有多快。
這個例子試圖說明兩個重要的概念:
(1)回調函數“handlePhoto”僅僅是一種“存放”操作的方式,而這些操作需要延遲一段時間之后進行。
(2)代碼的執行規則並不是按照閱讀代碼的“從上到下”的方式去遵守的,代碼執行會根據事情結束的時間跳轉嵌套。

三、我們如何解決“回調地獄”?

回調地獄的產生往往來源於對編碼練習的缺乏,幸運的是,寫出更好的代碼並不困難!
我們只需要遵循如下三個原則:

(1)保持代碼淺顯易讀

下面是一個雜亂的JavaScript代碼,這個代碼用於通過使用“  browser-request ”從瀏覽器想服務端提交一個Ajax請求:
var form = document.querySelector('form')
form.onsubmit = function (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function (err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}
這段代碼有兩個匿名函數,我們給他們賦上名字吧!
var form = document.querySelector('form')
form.onsubmit = function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function postResponse (err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}
正如你看到的,給函數進行命名是一件超級簡單的事情,而且會立刻體驗到幾個好處:

1)感謝這些具有描述性意義的函數名稱把,這些名稱使代碼更加容易地閱讀;

2)當異常發生的時候,你會在異常堆棧中看到確切的函數名稱,而不是“anonymous”之類的名字;

3)你可以把函數移動出去,並且通過名字去引用他們;

現在,我們把這兩個函數移動到我們程序的最頂層:
 
document.querySelector('form').onsubmit = formSubmit
function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}
function postResponse (err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}
注意一下,函數聲明在這里被移動到了文件最底部,這個要感謝  function hoisting.

(2)模塊化

這個是最重要的部分:任何人都有能力創建模塊。引用  Isaac Schlueter (來源於node.js項目)的話說:

Write small modules that each do one thing, and assemble them into other modules that do a bigger thing. You can't get into callback hell if you don't go there.

“編寫一個個小的模塊,每個模塊完成一件事情,然后把他們組裝起來,去完成一個更大的事情,回調地獄這個坑,你不去往那走,你是不會陷進去的”。
讓我們從上面的代碼中提取模板代碼,然后通過拆分到一組文件中的方式,將這些模板代碼組裝成module。我會展示一個module的格式,這種格式既可以用於瀏覽器的代碼,也可以用在服務端。
這個是一個新的文件,叫做“formuploader.js”,里面包含兩個從前面代碼中提取的兩個函數:
module.exports.submit = formSubmit
function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}
function postResponse (err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}
其中“module.exports”部分是一個node.js模塊系統的一個例子,electron 和 瀏覽器 使用 browserify 。我非常喜歡這種模塊化,因為它可以工作在任何地方,而且非常簡單,並且不需要復雜的配置文件或者腳本。
現在我們有了“formuploader.js”(並且已經作為一個腳本,在頁面加載完成之后載入到了頁面中),我們只需要“require”這個模塊並且使用它!這個是一個我們的程序中具體代碼的樣子:
var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit
這樣,我們的程序僅僅需要兩行代碼,並且有如下的好處:

1)對於一個新的開發者來說,更加容易理解了 ------ 他們不用深陷於“被迫通讀全部“formuploader”函數”。

2)“formuploader”可以用於其他地方,不用重復編寫代碼,並且可以輕松地分享到github或者npm上去。

(3)處理每一個獨立的異常

錯誤(error)有很多種類型:程序員犯的語法錯誤(往往在第一次嘗試運行程序的時候會被發現);程序員犯的運行時錯誤(程序可以運行但是里面有一個bug會把事情弄糟);其他情況下的平台錯誤比如無效的文件權限,硬件驅動失效、網絡連接異常等問題。這個部分主要針對最后一類錯誤(error)。
前面兩條原則可以讓你的代碼具有可讀性,但是這一條可以讓你的代碼保持穩定健壯。當你在使用回調函數(callback)的時候,講道理你其實是在和任務打交道,這些任務都是被分發給回調函數,並且回調函數會在后台執行,然后這個任務要么執行成功,要么由於失敗而終止。任何有經驗的開發者都會告訴你:你永遠不會知道錯誤是誰么時候會發生,你只有去假定他們會一直出現。
目前在回調函數中處理錯誤最流行的方式是Node.js風格:所有的回調函數的第一個參數永遠是給“error”保留着。
 var fs = require('fs')
 
fs.readFile('/Does/not/exist', handleFile)
 
function handleFile (error, file) {
if (error) return console.error('Uhoh, there was an error', error)
// otherwise, continue on and use `file` in your code
}
在第一個參數中使用“error”是一個簡單方便的鼓勵記得處理錯誤的一個方式。如果把這個參數放到第二的位置,你在寫代碼的時候往往會容易忽略第二個“error”參數,而只關注第一個參數,比如“function handleFile(file){}”。
Code linters(檢查代碼的小工具)也可以通過配置,實現提醒你要處理這些回調函數錯誤。最簡單的一個小工具就是  standard。這個工具你僅僅只需要在你的代碼文件的路徑中執行 “$ standard”命令,它就會把你每一個沒有進行錯誤處理的回調函數標記出來。

四、總結

1、不要嵌套函數。給這些函數進行命名,並且放到你的程序的最頂層。

2、使用 function hoisting(函數提升)機制將你的函數移到文件的末尾。

3、在每一個回調函數中去處理每一個錯誤。可以使用一個代碼檢查工具去幫你完成這個事情。

4、創建可以服用的函數,並且把他們放置在一個模塊中,這樣可以提高代碼可讀性。把代碼分割成一個個小的部分,可以幫助你更好的處理error,測試,強迫你去為你的代碼創建一個穩定的、文檔完善的公共API模塊,而且有助於代碼的重構。

最重要的避免回調地獄的方面就是,移出你的函數,這樣程序的流程可以更容易理解,新手也就不用去啃每一個函數究竟是干什么的。
從現在開始,你首先就可以把函數移到文件的底部,然后逐漸地把函數移到另一個文件中並且使用類似“require('./photo-helpers.js')”的方式去關聯,最終,把他們放進一個獨立的模塊比如“require('image-resize')”.

下面是一些創建模塊的一些原則:

1、通過把一些經常重復使用的代碼封裝成一個函數;

2、當你的函數(或者一組具有類似主題功能的函數)足夠大的時候,移動到另一個文件,並且通過“module.exports“的方式去發布,你可以使用類似“require('./photo-helpers.js')”的方式去關聯這個文件。

3、如果你的代碼可以用於很多個項目的時候,你需要提供“readme”文件、測試以及“package.json”文件,並且把他們發布到github和npm中。

4、一個優秀的模塊是很小的而且只聚焦於一個問題;

5、JavaScript的模塊中的一個獨立的文件行數不應該超過150行;

5、在整個JavaScript的文件結構組織中,一個模塊不應該擁有超過一層的嵌套文件夾。如果這種情況發生了,那么意味着整個模塊要做的事情有點過多了。

6、讓更有經驗的程序員給你演示下好的模塊構建的方式,直到你了解究竟什么是優秀的模塊。如果有一個模塊,你需要花不止幾分鍾的時間去了解它是干嘛的,那么這個模塊並不是一個多么好的模塊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM