翻譯自:http://callbackhell.com/,水平有限,做個人理解之用。
這是一個編寫異步JavaScript程序的指導手冊。
一、什么是回調地獄?
異步的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)) } }) }) } })
有沒有看到這些以"})"結尾的金字塔結構?這個形狀為“親切地”稱為回調地獄。
二、什么是callback(回調)?
“callback"僅僅只是一種使用JavaScript函數的一種通用稱呼。在JavaScript語言中,並沒有一種特定的東西稱之為“callback”,這個只是一種方便的稱呼。不同於大部分立即返回結果的函數,這些使用callback的函數需要消耗一些時間才能返回結果。“asynchronous”(異步),或者簡稱為“async”僅僅表示需要花費一些時間,或者是“在未來發生,而不是現在”。通常情況下,callback僅僅用於操作I/O的時候使用到。比如下載、讀寫文件、與數據庫交互等。
當調用一個普通的函數的時候,你可以這樣使用返回值:
var result = multiplyTwoNumbers(5, 10) console.log(result) // 50 gets printed out
然而,異步函數,也就是使用了callback函數的不會立刻返回任何東西。
var photo = downloadPhoto('http://coolcats.com/cat.gif') // photo is 'undefined'!
在這種情況下,下載gif文件會花費相當長的時間,而且你並不希望你的程序在等待下載結束的過程中處於“暫停”(也就是阻塞,block)狀態。
相反,你可以把下載結束后需要執行的操作存放到一個函數中,這個就是callback(回調)!你提供了一個“downloadPhoto”的函數,並且這個函數會在下載完成的時候執行callback(call you back later)函數,並且傳遞photo參數(或者出錯的時候返回一個錯誤信息)。
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')
人們在嘗試理解“callback”這個概念的最大困難之處在於,程序運行的過程中,程序中的代碼,是怎么樣按照規則執行的。在這個例子中,有三件主要的代碼段會發生:首先 “handlePhoto”函數被申明,然后是“downloadPhoto”函數會被調用並且把“handlePhoto”函數作為回調函數“callback”參數傳遞進入,最后,打印一句話“Download started”。
要注意以下,“handlePhoto”並沒有立即被調用,只是創建並且作為一個參數傳遞給了“downloadPhoto”。但是,一旦“downloadPhoto”函數執行完成之后,”handlePhoto“就會運行。這個取決於網絡連接到底有多快。
這個例子試圖說明兩個重要的概念:
(1)回調函數“handlePhoto”僅僅是一種“存放”操作的方式,而這些操作需要延遲一段時間之后進行。
(2)代碼的執行規則並不是按照閱讀代碼的“從上到下”的方式去遵守的,代碼執行會根據事情結束的時間跳轉嵌套。
三、我們如何解決“回調地獄”?
回調地獄的產生往往來源於對編碼練習的缺乏,幸運的是,寫出更好的代碼並不困難!
我們只需要遵循如下三個原則:
(1)保持代碼淺顯易讀
下面是一個雜亂的JavaScript代碼,這個代碼用於通過使用“
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 }) }
正如你看到的,給函數進行命名是一件超級簡單的事情,而且會立刻體驗到幾個好處:
1)感謝這些具有描述性意義的函數名稱把,這些名稱使代碼更加容易地閱讀;
2)當異常發生的時候,你會在異常堆棧中看到確切的函數名稱,而不是“anonymous”之類的名字;
3)你可以把函數移動出去,並且通過名字去引用他們;
現在,我們把這兩個函數移動到我們程序的最頂層:
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
}
注意一下,函數聲明在這里被移動到了文件最底部,這個要感謝
function hoisting.
(2)模塊化
這個是最重要的部分:任何人都有能力創建模塊。引用
Isaac Schlueter (來源於node.js項目)的話說:
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.
“編寫一個個小的模塊,每個模塊完成一件事情,然后把他們組裝起來,去完成一個更大的事情,回調地獄這個坑,你不去往那走,你是不會陷進去的”。
讓我們從上面的代碼中提取模板代碼,然后通過拆分到一組文件中的方式,將這些模板代碼組裝成module。我會展示一個module的格式,這種格式既可以用於瀏覽器的代碼,也可以用在服務端。
這個是一個新的文件,叫做“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模塊系統的一個例子,electron 和 瀏覽器 使用
browserify 。我非常喜歡這種模塊化,因為它可以工作在任何地方,而且非常簡單,並且不需要復雜的配置文件或者腳本。
現在我們有了“formuploader.js”(並且已經作為一個腳本,在頁面加載完成之后載入到了頁面中),我們只需要“require”這個模塊並且使用它!這個是一個我們的程序中具體代碼的樣子:
var formUploader = require('formuploader') document.querySelector('form').onsubmit = formUploader.submit
這樣,我們的程序僅僅需要兩行代碼,並且有如下的好處:
1)對於一個新的開發者來說,更加容易理解了 ------ 他們不用深陷於“被迫通讀全部“formuploader”函數”。
2)“formuploader”可以用於其他地方,不用重復編寫代碼,並且可以輕松地分享到github或者npm上去。
(3)處理每一個獨立的異常
錯誤(error)有很多種類型:程序員犯的語法錯誤(往往在第一次嘗試運行程序的時候會被發現);程序員犯的運行時錯誤(程序可以運行但是里面有一個bug會把事情弄糟);其他情況下的平台錯誤比如無效的文件權限,硬件驅動失效、網絡連接異常等問題。這個部分主要針對最后一類錯誤(error)。
前面兩條原則可以讓你的代碼具有可讀性,但是這一條可以讓你的代碼保持穩定健壯。當你在使用回調函數(callback)的時候,講道理你其實是在和任務打交道,這些任務都是被分發給回調函數,並且回調函數會在后台執行,然后這個任務要么執行成功,要么由於失敗而終止。任何有經驗的開發者都會告訴你:你永遠不會知道錯誤是誰么時候會發生,你只有去假定他們會一直出現。
目前在回調函數中處理錯誤最流行的方式是Node.js風格:所有的回調函數的第一個參數永遠是給“error”保留着。
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 }
在第一個參數中使用“error”是一個簡單方便的鼓勵記得處理錯誤的一個方式。如果把這個參數放到第二的位置,你在寫代碼的時候往往會容易忽略第二個“error”參數,而只關注第一個參數,比如“function handleFile(file){}”。
Code linters(檢查代碼的小工具)也可以通過配置,實現提醒你要處理這些回調函數錯誤。最簡單的一個小工具就是
standard。這個工具你僅僅只需要在你的代碼文件的路徑中執行 “$ standard”命令,它就會把你每一個沒有進行錯誤處理的回調函數標記出來。
四、總結
1、不要嵌套函數。給這些函數進行命名,並且放到你的程序的最頂層。
2、使用 function hoisting(函數提升)機制將你的函數移到文件的末尾。
3、在每一個回調函數中去處理每一個錯誤。可以使用一個代碼檢查工具去幫你完成這個事情。
4、創建可以服用的函數,並且把他們放置在一個模塊中,這樣可以提高代碼可讀性。把代碼分割成一個個小的部分,可以幫助你更好的處理error,測試,強迫你去為你的代碼創建一個穩定的、文檔完善的公共API模塊,而且有助於代碼的重構。
最重要的避免回調地獄的方面就是,移出你的函數,這樣程序的流程可以更容易理解,新手也就不用去啃每一個函數究竟是干什么的。
從現在開始,你首先就可以把函數移到文件的底部,然后逐漸地把函數移到另一個文件中並且使用類似“require('./photo-helpers.js')”的方式去關聯,最終,把他們放進一個獨立的模塊比如“require('image-resize')”.
