回調地獄

本文首發於微信公衆號平臺(itclancoder),若是你想閱讀體驗更好,能夠戳連接回調地獄node

從前一文中你真的瞭解回調咱們已知道回調函數是必須得依賴另外一個函數執行調用,它是異步執行的,也就是須要時間等待,典型的例子就是Ajax應用,好比http請求,在不刷新瀏覽器的狀況下,當你執行DOM事件時,好比頁面上點擊某連接,回車等事件操做,瀏覽器會悄悄向服務端發送若干http請求,攜帶後臺可識別的參數,等待服務器響應返回數據,這個過程是異步回調的,當許多功能須要連續調用,環環相扣依賴時,它就相似下面的代碼,代碼所有一層一層的嵌套,看起來就很龐大,很噁心,就產生了回調地獄.本文,將爲你揭曉怎麼避免回調地獄,您將在本文中瞭解到如下內容:react

  • 什麼是回調地獄(函數做爲參數層層嵌套)git

  • 什麼是回調函數(一個函數做爲參數須要依賴另外一個函數執行調用)程序員

  • 如何解決回調地獄github

    • 保持你的代碼簡短(給函數取有意義的名字,見名知意,而非匿名函數,寫成一大坨)數據庫

    • 模塊化(函數封裝,打包,每一個功能獨立,能夠單獨的定義一個js文件Vue,react中經過import導入就是一種體現)npm

    • 處理每個錯誤編程

    • 建立模塊時的一些經驗法則json

    • 承諾/生成器/ES6等promise

  • Promises:編寫異步代碼的一種方式,它仍然以自頂向下的方式執行,而且因爲鼓勵使用try / catch樣式錯誤處理而處理更多類型的錯誤

  • Generators:生成器讓你「暫停」單個函數,而不會暫停整個程序的狀態,但代碼要稍微複雜一些,以使代碼看起來像自上而下地執行

  • Async functions:異步函數是一個建議的ES7功能,它將以更高級別的語法進一步包裝生成器和繼承

什麼是「回調地獄」?

異步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))
        }
      })
    })
  }
})
複製代碼

看到最後的金字塔形狀和全部})?這被親切地稱爲回調地獄

回調地獄的緣由是,當人們試圖以一種從上到下的視覺方式執行JavaScript的方式編寫JavaScript時。不少人犯這個錯誤!在C,Ruby或Python等其餘語言中,指望第1行發生的任何事情都會在第2行的代碼開始運行以前完成,依此類推。正如你將會學到的,JavaScript是不一樣的

什麼是回調函數?

回調只是使用JavaScript函數的慣例的名稱。 JavaScript語言中沒有特別的東西叫作「回調」,它只是一個約定。不像大多數函數那樣當即返回一些結果,使用回調函數須要一些時間來產生結果。 意思是「須要一些時間」或「未來會發生,而不是如今」。一般回調僅在進行I / O時使用,例以下載東西,閱讀文件,與數據庫交互等

當你調用一個普通的函數時,你可使用它的返回值

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功能,它會在下載完成時運行你的回調(例如'之後再打電話給你'),而且傳遞照片(或者若是出現錯誤,會出錯)

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函數並傳遞handlePhoto做爲其回調函數,最後打印出「Download started」

請注意,handlePhoto還沒有被調用,它只是被建立並做爲回調傳入downloadPhoto。但直到downloadPhoto完成其任務後才能運行,這可能須要很長時間,具體取決於Internet鏈接的速度

這個例子是爲了說明兩個重要的概念

  • handlePhoto回調只是稍後存儲一些事情的一種方式
  • 事情發生的順序不是從頂部到底部讀取,而是基於事情完成時跳轉

我該如何解決回調地獄?

回調地獄是因爲糟糕的編碼習慣形成的。幸運的是,編寫更好的代碼並不困難! 你只需遵循三條規則:

1. 保持你的代碼簡短

這裏有一些凌亂的瀏覽器JavaScript,它使用瀏覽器請求向服務器發送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
  })
}
複製代碼

這段代碼有兩個匿名函數。讓咱們給他們的名字formSubmit與postResponse

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
  })
}
複製代碼

正如你所看到的,命名函數很是簡單而且有一些直接的好處

  • 因爲描述性功能名稱,使代碼更容易閱讀
  • 當發生異常時,你將得到引用實際函數名稱而不是「匿名」的堆棧跟蹤
  • 容許你移動功能並按名稱引用它們

如今咱們能夠將這些功能移到咱們程序的頂層

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. 模塊化

這是最重要的部分:任何人都有能力建立模塊。引用(node.js項目的)Isaac Schlueter的話:「編寫一個小模塊,每一個模塊都作一件事,而後將它們組裝成其餘模塊,作更大的事情。若是你不去那裏,你不能進入回調地獄

讓咱們從上面取出樣板代碼,並將其分紅幾個文件,將其轉換爲模塊。我將展現一個適用於瀏覽器代碼或服務器代碼的模塊模式(或者適用於二者的代碼)

這是一個名爲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函數而陷入困境
  • ormuploader能夠在其餘地方使用,無需複製代碼,而且能夠輕鬆地在github或npm上共享

3. 處理每個錯誤

有不一樣類型的錯誤:由程序員形成的語法錯誤(一般在你嘗試首次運行程序時發生),程序員形成的運行時錯誤(代碼已運行但存在致使某些事情混亂的錯誤),平臺錯誤由無用的文件權限,硬盤驅動器故障,無網絡鏈接等引發的。這部分只是爲了解決最後一類錯誤

前兩條規則主要是關於讓你的代碼可讀,但這是關於讓代碼穩定的。在處理回調時,你根據定義處理已分派的任務,請在後臺執行某些操做,而後成功完成或因爲失敗而停止。任何有經驗的開發人員都會告訴你,你永遠沒法知道這些錯誤什麼時候發生,因此你必須對它們進行計劃

經過回調,處理錯誤的最多見方法是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
 }
複製代碼

有第一個參數是錯誤是一個簡單的慣例,鼓勵你記住處理你的錯誤。若是它是第二個參數,你能夠編寫像函數handleFile(file){}的代碼,而且更容易忽略錯誤

代碼庫也能夠配置爲幫助你記住處理回調錯誤。最簡單的使用稱爲標準。你所要作的就是在你的代碼文件夾中運行$ standard,它會向你顯示你的代碼中的每個回調,並帶有未處理的錯誤

小結

  1. 不要嵌套功能。給他們姓名並將他們放在程序的頂層
  2. 利用函數提高來利用你的優點來移動函數
  3. 處理每一個回調中的每個錯誤。使用標準來幫助你
  4. 建立可重用的函數並將它們放在模塊中以減小理解代碼所需的認知負載。將代碼分割成小塊這樣也能夠幫助您處理錯誤,編寫測試,強制您爲您的代碼建立穩定且文檔化的公共API,並有助於重構

避免回調地獄的最重要的方面是將功能移開,以便程序流程能夠更容易理解,而無需新手參與功能的全部細節以瞭解程序正在嘗試作什麼

你能夠先將函數移動到文件底部,而後使用require('./ photo-helpers.js')等相關需求將它們移動到另外一個文件中,而後將它們移動到獨立模塊像 require('image-resize'))

如下是建立模塊時的一些經驗法則:

  • 首先將重複使用的代碼移入一個函數
  • 當你的函數(或與同一主題相關的一組函數)變得足夠大時,將它們移動到另外一個文件中並使用module.exports將其公開。你可使用相對需求來加載它
  • 若是你有一些能夠在多個項目中使用的代碼,給它本身的readme,tests和package.json,並將它發佈到github和npm。這裏列出的具體方法有太多使人敬畏的好處
  • 一個好的模塊很小,專一於一個問題
  • 模塊中的單個文件不該超過150行左右的JavaScript
  • 一個模塊不該該有多於一個嵌套文件夾級別的文件夾。若是是這樣,它可能作了太多事情
  • 請你認識的更有經驗的編程人員向你展現優秀模塊的例子,直到你對他們的樣子有了一個好的想法。若是須要花費幾分鐘時間才能瞭解正在發生的事情,那麼它可能不是一個很好的模塊

承諾/生成器/ES6等呢

在研究更先進的解決方案以前,請記住,回調是JavaScript的基本組成部分(由於它們只是函數),你應該在學習更先進的語言特性以前學習如何讀寫它們,由於它們都依賴於對回調。若是你還不能編寫可維護的回調代碼,請繼續使用它

若是你真的但願你的異步代碼從頭至尾閱讀,你能夠嘗試一些奇特的東西。請注意,這些可能會引入性能和/或跨平臺運行時兼容性問題

  • Promises:是編寫異步代碼的一種方式,它仍然以自頂向下的方式執行,而且因爲鼓勵使用try / catch樣式錯誤處理而處理更多類型的錯誤
  • Generators生成器讓你「暫停」單個函數,而不會暫停整個程序的狀態,但代碼要稍微複雜一些,以使代碼看起來像自上而下地執行。
  • Async functions異步函數是一個建議的ES7功能,它將以更高級別的語法進一步包裝生成器和承諾。

總結

回調地獄最主要的就是由於功能邏輯代碼嵌套的層次太多,致使可讀性下降,維護困難,避免回調地獄的最重要的方面是將功能移開,保持代碼簡單,不嵌套並分紅小模塊,也就是多多進行代碼封裝,將你所要的屬性和方法用function關鍵字包裹起來,並且還要給它取一個有意義的名字,例如:頁面上彈框,顯示,隱藏,下拉等各個功能小模塊,分別用有名函數給包裹起來,少用匿名函數,以即可以重複的屢次使用,這也是能夠便於程序流程的理解

除了常見的一種回調函數做爲異步處理,還有promises,Generators,async是處理異步處理的方式,,關於這三個我也在學習當中,理論的東西雖是概念,沒有大量代碼的編寫,我的以爲是很難理解這些東西,可是代碼就是這些語言文字實實在在的轉化,騷年們,加油,加油....

原文閱讀出處

相關文章
相關標籤/搜索