回調地獄-編寫異步JavaScript指南

什麼是「回調地獄」?

異步Javascript代碼,或者說使用callback的Javascript代碼,很難符合咱們的直觀理解。不少代碼最終會寫成這樣:node

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

看到上面金字塔形狀的代碼和那些末尾良莠不齊的})了嗎?吐了!這就是廣爲人知的回調地獄了。
人們在編寫JavaScript代碼時,誤認爲代碼是按照咱們看到的代碼順序從上到下執行的,這就是形成回調地獄的緣由。在其餘語言中,例如C,Ruby或者Python,第一行代碼執行結束後,纔會開始執行第二行代碼,按照這種模式一直到執行到當前文件中最後一行代碼。隨着你學習深刻,你會發現JavaScript跟他們是不同的。git

什麼是回調(callback)?

某種使用JavaScript函數的慣例用法的名字叫作回調。JavaScript語言中沒有一個叫「回調」的東西,它僅僅是一個慣例用法的名字。大多數函數會馬上返回執行結果,使用回調的函數一般會通過一段時間後才輸出結果。名詞「異步」,簡稱「async」,只是意味着「這將花費一點時間」或者說「在未來某個時間發生而不是如今」。一般回調只使用在I/O操做中,例以下載文件,讀取文件,鏈接數據庫等等。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圖片可能須要很長的時間才能下載完成,但你不想你的程序在等待下載完成的過程當中停止(也叫阻塞)。異步

因而你把須要下載完成後運行的代碼存放到一個函數中(等待下載完成後再運行它)。這就是回調!你把回調傳遞給downloadPhoto函數,當下載結束,回調會被調用。若是下載成功,傳入photo給回調;下載失敗,傳入error給回調。async

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

人們理解回調的最大障礙在於理解一個程序的執行順序。在上面的例子中,發生了三件事情。ide

  1. 聲明handlePhoto函數
  2. downloadPhoto函數被調用而且傳入了handlePhoto最爲它的回調
  3. 打印出Download started

請你們注意,起初handlePhoto函數僅僅是被建立並被做爲回調傳遞給了downloadPhoto,它尚未被調用。它會等待downloadPhoto函數完成了它的任務纔會執行。這可能須要很長一段時間(取決於網速的快慢)。模塊化

這個例子意在闡明兩個重要的概念:函數

  1. handlePhoto回調只是一個存放未來進行的操做的方式
  2. 事情發生的順序並非直觀上看到的從上到下,它會當某些事情完成後再跳回來執行。

怎樣解決「回調地獄」問題?

糟糕的編碼習慣形成了回調地獄。幸運的是,編寫優雅的代碼不是那麼難!

你只須要遵循三大原則

1. 減小嵌套層數(Keep your code shallow)

下面是一堆亂糟糟的代碼,使用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
  })
}

如你所見,給匿名函數一個名字是多麼簡單,並且好處立竿見影:

  • 起一個一望便知其函數功能的名字讓代碼更易讀
  • 當拋出異常時,你能夠在stacktrace裏看到實際出異常的函數名字,而不是"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.模塊化(Modularize)

任何人都有有能力建立模塊,這點很是重要。Isaac Schlueter(NodeJS項目成員)說過「寫出一些小模塊,每一個模塊只作一件事情,而後把他們組合起來放入其餘的模塊作一個複雜的事情。只要你不想陷入回調地獄,你就不會落入那般田地。」

讓咱們把上面的例子修改一下,改成一個模塊。

下面是一個名爲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模塊化的用法。如今已經有了 formuploader.js 文件,咱們只須要引入它並使用它。請看下面的代碼:

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

咱們的應用只有兩行代碼而且還有如下好處:

  1. 方便新開發人員理解你的代碼 -- 他們不須要費盡力氣讀完formuploader函數的所有代碼
  2. formuploader能夠在其餘地方複用

3.處理每個異常(Handle every single error)

有三種不一樣類型的異常:語法異常,運行時異常和平臺異常。語法異常一般由開發人員在第一次解釋代碼時捕獲,運行時異常一般在代碼運行過程當中由於bug觸發,平臺異常一般因爲沒有文件的權限,硬盤錯誤,無網絡連接等問題形成。這一部分主要來處理最後一種異常:平臺異常。

前兩個大原則意在提升代碼可讀性,可是第三個原則意在提升代碼的穩定性。在你與回調打交道的時候,你一般要處理髮送請求,等待返回或者放棄請求等任務。任何有經驗的開發人員都會告訴你,你歷來不知道哪裏回出現問題。因此你有必要提早準備好,異常老是會發生。

把回調函數的第一個參數設置爲error對象,是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對象是一個約定俗成的慣例,提醒你記得去處理異常。若是它是第二個參數,你更容易把它忽略掉。

總結

  • 不要嵌套使用函數。給每一個函數命名並把他們放在你代碼的頂層
  • 利用函數提高。先使用後聲明。
  • 處理每個異常
  • 編寫能夠複用的函數,並把他們封裝成一個模塊
相關文章
相關標籤/搜索