【翻譯】關於回調地獄

回調地獄

JavaScript異步程序書寫指南javascript

什麼是「回調地獄」?

咱們很難一眼就看懂異步JavaScript,或者是使用回調函數的JavaScript程序。例以下面這段代碼:java

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))
        }
      })
    })
  }
})

這個一堆以})結尾的金字塔,咱們很親切地稱它爲——「回調地獄」。node

之因此會出現回調地獄,是由於咱們寫JavaScript通常是視覺上的從上到下書寫。不少人犯了這個錯誤!在例如C、Ruby或者Python等其餘語言,在第二行代碼運行以前,第一行代碼確定已經運行完了。然而如後面所說的,JavaScript是不一樣的。git

什麼是回調函數?

回調函數是JavaScript里約定俗成的一個名稱。實際上並不存在肯定的「回調函數」,只是你們就管那個位置的函數做回調函數。與大多數運行後馬上給出結果的函數不一樣,使用回調的函數要花一些時間才能得出結果。「異步」這個詞就是表明‘要花時間,未來運行’。一般回調函數會用在下載文件、讀取文件、或者數據庫相關事務等。程序員

當你調用一個普通函數,你能夠馬上獲得它的值:github

var result = multiplyTwoNumbers(5, 10)
console.log(result)
// 50 gets printed out

而使用回調的函數不能馬上獲得反饋。數據庫

var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo is 'undefined'!

這個時候,這張gif可能要下載好久,你總不能讓程序什麼都不幹停下來就等它下載完。npm

相反,你能夠儲存下載完後觸發的代碼到一個函數裏,這就是回調函數!把這些代碼寫進downloadPhoto函數,下載成功後,會運行回調函數。json

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')

咱們理解回調最難的地方就是理解程序的運行順序。例子中發生了三個主要事件,首先是handlePhoto函數被聲明,而後做爲回調函數被downloadPhoto函數調用,最後控制檯打印出'Download started'promise

注意handlePhoto尚未被調用,它只是被建立而後最爲回調函數傳入downloadPhoto。直到downloadPhoto完成下載,他都不會運行。

這個例子說明兩個問題:

  • handlePhoto(回調函數)只是儲存了將要運行的東西

  • 不要從上到下閱讀程序,程序會根據事情完成而跳轉

怎麼修復回調地獄?

你只須要跟着一下三步走:

1.減小代碼嵌套

如下是一些用於AJAX的瀏覽器端代碼(使用browser-request):

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
  })
}

大家看,給函數命名很簡單,可是好處可很多:

  • 有了函數名,能夠很容易知道這段代碼的做用

  • 在控制檯調試出錯的時候,控制檯會告訴你是哪一個函數出錯了,而不是一個匿名函數(anonymous)

  • 可讓你把這些函數移動到合適的位置,使用的時候用函數名調用就能夠了

如今咱們都寫到程序最外層:

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
}

注意,函數聲明在底部,卻仍然能調用,這得益於函數提高

2.模塊化

用上面的例子,咱們將把它拆分紅多個文件,我會告訴你怎麼把他作成模塊。

建立一個包含前面兩個函數的新文件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的模塊系統,可使用在node、Electron,瀏覽器上(藉助browserify)。我十分喜歡這種風格,由於哪兒都能用,並且易於理解,不用依賴於其餘複雜設置。

咱們獲得了formuploader.js,只要引入並使用它就能夠了!操做以下:

var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

如今咱們的代碼只有兩行,有如下好處:

  • 易於新開發者理解,他們不會爲讀取全部的formuploader函數而陷入困境。

  • formuploader不用複製粘貼代碼,只要在github或者npm下載分享的代碼就能夠了。

3.處理每個錯誤

常見錯誤有幾種

  • 語法錯誤(運行失敗)

  • 運行時錯誤(能夠運行可是有bug)

  • 平臺錯誤(文件權限問題、磁盤問題、網絡問題)

前兩條規則主要是提升你的代碼的可讀性,而這條是讓你的代碼更穩定。在處理回調時,您將根據定義處理髮送的任務,在後臺執行某些操做,最後成功完成或失敗停止。任何有經驗的開發人員都會告訴你,你永遠不會知道這些錯誤發生何時發生,因此在問題出現時都必須有所對策。

最經常使用的回調錯誤處理是Node.js風格,也就是回調函數的第一個參數老是錯誤參數。

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是一個簡單的共識,這樣作能夠提醒你必須處理你的錯誤。若是是第二個參數的話你很容易把代碼寫成function handleFile (file) { }而後就忘了處理錯誤。
代碼規範化工具也能夠提醒你添加回調錯誤處理,最簡單的方法之一是使用standard。只是在你的文件目錄運行 $ standard就能檢查你的代碼有沒有缺乏錯誤處理。

總結

  1. 不要嵌套函數,命名後調用更好

  2. 使用函數提高

  3. 處理回調函數的每個錯誤

  4. 建立可重用函數,寫成模塊,讓你更容易讀懂代碼。把你的代碼拆分紅小塊能夠幫助你處理錯誤,寫測試,重構,方便爲你的代碼寫更穩定的API

避免回調地獄的最重要的是移動函數,以便程序流程能夠更容易地被理解,其餘程序員能夠不翻遍整個文件就能知道這段程序的功能。

你能夠先把函數移動到底部,而後逐漸把函數寫到模塊文件裏,而後使用require引入它(就像引用其餘npm模塊同樣)。

一些寫模塊的經驗:

  • 先把常常重複使用的功能寫成一個函數

  • 當這個函數寫得夠大以後,把他移動到另外一個文件,用module.exports暴露它,而後用require引入

  • 若是你的代碼是通用的,能夠寫readme文件和package.json發佈到npm或者github

  • 一個好模塊,體積要小,並且針對只一個問題

  • 模塊中的單個文件不該超過約150行

  • 模塊不該該有多個級別的嵌套文件夾,其中包含JavaScript文件。若是是這樣,它可能作的太多了

  • 讓有經驗的程序員介紹你一些好用的模塊,嘗試理解這個模塊的功能,若是花了幾分鐘的話,這個模塊可能就不夠好了

關於promise/生成器/ES6?

在查看更高級的解決方案以前,請記住,回調是JavaScript的一個基本部分(由於它們只是函數),你應該學習如何讀寫它們,而後再轉向更高級的語言功能,由於它們依賴於對回調的理解。若是您還不能寫可維護的回調代碼,請繼續努力學習!

若是你真的想你的異步代碼能夠「從上至下閱讀」,你能夠試試這些美妙的方法。注意,這些功能在不一樣平臺會有兼容性問題,使用前請先調查清楚!

Promise就是一種讓你從上至下寫回調函數的方法,它鼓勵你使用try/catch處理更多類型的錯誤。

Generator可讓你「暫停」一個函數(而不暫停整個程序),它也能你從上至下寫異步函數,可是代價是代碼有點複雜難以理解。wat就是使用這個方法。

Async functions是ES7的特性,是生成器和promise更高級的封裝,有興趣本身谷歌一下唄。

就我我的而言,我使用回調函數處理90%的異步代碼,當事情變得複雜時,依靠一些庫,例如run-parallel或者run-series。我不認爲研究回調 vs promise vs 其餘什麼方法對我來講有什麼幫助,最重要的仍是保持代碼簡單,不嵌套,並分紅小模塊。

不管你選擇何種方法,請始終處理每一個錯誤,並保持代碼簡潔

記住,只有能夠防止回調地獄和森林火災。

原文: http://callbackhell.com/
本文github地址: https://github.com/ssshooter/...

相關文章
相關標籤/搜索