回撥地獄

itclanCoder發表於2019-03-04

本文首發於微信公眾號平臺(itclancoder),如果你想閱讀體驗更好,可以戳連結回撥地獄

從前一文中你真的瞭解回撥我們已知道回撥函式是必須得依賴另一個函式執行呼叫,它是非同步執行的,也就是需要時間等待,典型的例子就是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
  })
}
複製程式碼

這段程式碼有兩個匿名函式。讓我們給他們的名字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是處理非同步處理的方式,,關於這三個我也在學習當中,理論的東西雖是概念,沒有大量程式碼的編寫,個人覺得是很難理解這些東西,但是程式碼就是這些語言文字實實在在的轉化,騷年們,加油,加油….

原文閱讀出處

相關文章