回調地獄


前言

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

  • 什么是回調地獄(函數作為參數層層嵌套)

  • 什么是回調函數(一個函數作為參數需要依賴另一個函數執行調用)

  • 如何解決回調地獄

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

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

    • 處理每一個錯誤

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

    • 承諾/生成器/ES6等

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

這段代碼有兩個匿名函數。讓我們給他們的名字

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')等相關需求將它們移動到另一個文件中,然后將它們移動到獨立模塊like 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是處理異步處理的方式,,關於這三個我也在學習當中,理論的東西雖是概念,沒有大量代碼的編寫,個人覺得是很難理解這些東西,但是代碼就是這些語言文字實實在在的轉化,騷年們,加油,加油....

 


免責聲明!

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



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