[譯] 回調地獄——JavaScript非同步編程指南

來源:互聯網
上載者:User

標籤:工具   含義   引入   msu   rom   預測   sage   str   載入完成   

原文:Callback Hell

 

什麼是 “回調地獄”?

在 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,最終便製造了回調地獄。

在一些其它的程式設計語言(如 C、Ruby、Python)中,會確保第 1 行代碼已執行完成,並且檔案也已載入完畢之後,才開始執行第 2 行代碼。但如你所知,JavaScript 並非如此。

 

什麼是回調?

回調(callbacks)只是函數的一種用法的通用稱呼,在 JavaScript 中,並沒有一個特定的東西叫 “回調”,它僅僅是一個約定好的稱呼。

不同於那些立即返回結果的函數,回呼函數需要一定的時間來獲得結果。

譯者註:根據 wiki 上對 callback 的描述,回調分為同步回調和非同步回調,這裡應該是特指非同步回調。詳情見:Callback (computer programming)。 

This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback.

“asynchronous(非同步)” ,也叫 “async”,表示 “需要耗費一定的時間” 或者 “發生在未來,而不是現在”。

在處理 I/O 時,通常會使用到回調,如下載、讀取檔案、與資料庫互動等。

調用一個普通的函數時,我們可以直接使用其傳回值:

var result = multiplyTwoNumbers(5, 10)console.log(result)// 控制台列印出 50

而使用回調的非同步函數,不會立即返回結果:

var photo = downloadPhoto(‘http://coolcats.com/cat.gif‘)// photo 未定義!

下載 gif 檔案可能需要很長的時間,而你肯定不希望程式在下載過程中處於暫停(即 “block(阻塞)”)狀態。

你可以把下載完成後需要執行的操作存放在一個函數中,這就是回呼函數。把它傳遞給  downloadPhoto ,當下載完成時,downloadPhoto 會執行這個回呼函數(callback,call you back later),並把 error(錯誤資訊)或 photo(圖片資料)傳遞給它。

downloadPhoto(‘http://coolcats.com/cat.gif‘, handlePhoto)function handlePhoto (error, photo) {  if (error) console.error(‘下載出錯!‘, error)  else console.log(‘下載完成‘, photo)}console.log(‘開始下載‘)

理解回調最大的痛點,在於搞清楚程式運行時代碼的執行順序。在這個例子中主要有三個關鍵點:首先聲明了  handlePhoto  函數,然後調用了  downloadPhoto  函數並將  handlePhoto  作為回呼函數傳遞給它,最後 “開始下載” 被列印出來。

注意此時  handlePhoto  還沒有被調用,只是建立並作為回呼函數傳遞給了  downloadPhoto ,在  downloadPhoto  完成任務後才會被執行,這取決於網速有多快。

這個例子想要傳達兩個重要的概念:

  • 回呼函數  handlePhoto  只是存放操作的一種方式,可以讓這些操作在一段時間後(滿足了特定條件)才被執行。
  • 代碼執行的順序不是按照視覺上的自上而下,而是基於邏輯的完成時機跳躍式觸發。

譯者註:關於非同步回調的執行原理,可以參考 [譯] JavaScript 的事件迴圈。

 

如何處理回調地獄?

回調地獄的產生源於開發經驗的不足,幸運的是想要寫好這些代碼並不困難。你只要遵循下面三個原則:

 

1、避免函數嵌套

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

如你所見,給函數命名非常簡單,卻立竿見影:

  • 帶有描述性含義的函數名,讓代碼更容易閱讀
  • 出現異常時,可以在堆棧中查看到一個確切的函數名而不是 “anonymous”
  • 可以很方便地移動函數,然後通過函數名來引用

現在,我們可以把這些函數移到程式最外層:

document.querySelector(‘form‘).onsubmit = formSubmitfunction 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}

注意這裡把函式宣告移到了檔案的底部,這得益於函式宣告提升(function hoisting)。

 

2、模組化

這是最重要的一點:人人皆可搞模組(即程式碼程式庫)。

Anyone is capable of creating modules (aka libraries)

引用(node.js 項目的)Isaac Schlueter 的話:“編寫職責單一的小模組,組裝起來以實現更大的功能。回調地獄你不去碰它就不會掉進去。”

Write small modules that each do one thing, and assemble them into other modules that do a bigger thing. You can‘t get into callback hell if you don‘t go there.

讓我們從上面的代碼中提取出樣板代碼,分割成兩個檔案,把它變成一個模組。我將展示一個模組模式,它既可用於瀏覽器,也可用於服務端。

建立一個檔案叫  formuploader.js ,包含了從上面的代碼中提取出來的兩個函數:

module.exports.submit = formSubmitfunction 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 (並且作為頁面的一個外聯指令碼已載入完成),我們只需要引入(require)這個模組並使用它!

程式的具體代碼如下:

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

程式僅僅只需要兩行代碼,而且還有以下好處:

  • 對於新的開發人員更加容易理解——他們不用深陷於 “被迫通讀全部  formuploader  函數”
  •  formuploader  可以用於其它地方而不需要複製代碼,而且也更容易分享到 github 或 npm

 

3、處理每一個錯誤

錯誤有許多類型:語法錯誤(通常只要運行程式就能被捕獲)、執行階段錯誤(程式運行正常但存在一些 bug 會引起邏輯混亂)、平台錯誤(如無效的檔案許可權、硬體驅動失效、網路連接異常等)。這一部分主要針對最後一類錯誤。

前面兩個原則可以讓你的代碼更具可讀性,而這個原則,可以讓你的代碼更具穩定性。

回呼函數被定義和分配後,會在後台執行,然後成功完成或者失敗中止。任何有經驗的開發人員都會告訴你:你永遠無法預測錯誤何時會發生,你只能假設它一定會發生。

在回呼函數中處理錯誤的方式,最流行的是 Node.js 風格:回呼函數第一個參數永遠是 “error”。

 var fs = require(‘fs‘) fs.readFile(‘/Does/not/exist‘, handleFile) function handleFile (error, file) {   if (error) return console.error(‘臥槽,出錯了‘, error)   // 正常,可以在代碼中使用 `file` 了 }

把第一個參數設定為  error ,是鼓勵你記得處理錯誤的一個簡單的約定。如果把它設定為第二個參數,你可能會把代碼寫成  function handleFile(file){} ,而忽略了錯誤處理。

編碼規範檢查工具(Code linters)也可以通過配置來協助你記得處理回調錯誤。使用最簡單的一個是 standard,你只需要在代碼目錄中執行  $ standard  命令,它就會把代碼中沒有處理錯誤的回呼函數全部顯示出來。

 

要點

(施工中……)

 

更多閱讀

我的 回調更詳細的介紹

nodeschool 上的教程

browserify-handbook 編寫模組代碼的樣本

 

關於 promises/generators/ES6

(施工中……)

 

記住,只有你可以防止回調地獄和森林火災

你可以在這個 github 上查看相關源碼。

[譯] 回調地獄——JavaScript非同步編程指南

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.