標籤:工具 含義 引入 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非同步編程指南