如果你想閱讀體驗更好,可以戳鏈接回調(diào)地獄
前言
從前一文中你真的了解回調(diào)我們已知道回調(diào)函數(shù)是必須得依賴另一個函數(shù)執(zhí)行調(diào)用,它是異步執(zhí)行的,也就是需要時間等待,典型的例子就是Ajax應(yīng)用,比如http請求,在不刷新瀏覽器的情況下,當(dāng)你執(zhí)行DOM事件時,比如頁面上點擊某鏈接,回車等事件操作,瀏覽器會悄悄向服務(wù)端發(fā)送若干http請求,攜帶后臺可識別的參數(shù),等待服務(wù)器響應(yīng)返回數(shù)據(jù),這個過程是異步回調(diào)的,當(dāng)許多功能需要連續(xù)調(diào)用,環(huán)環(huán)相扣依賴時,它就類似下面的代碼,代碼全部一層一層的嵌套,看起來就很龐大,很惡心,就產(chǎn)生了回調(diào)地獄.本文,將為你揭曉怎么避免回調(diào)地獄,您將在本文中了解到以下內(nèi)容:
什么是回調(diào)地獄(函數(shù)作為參數(shù)層層嵌套)
什么是回調(diào)函數(shù)(一個函數(shù)作為參數(shù)需要依賴另一個函數(shù)執(zhí)行調(diào)用)
-
如何解決回調(diào)地獄
保持你的代碼簡短(給函數(shù)取有意義的名字,見名知意,而非匿名函數(shù),寫成一大坨)
模塊化(函數(shù)封裝,打包殴蹄,每個功能獨立,可以單獨的定義一個js文件Vue,react中通過import導(dǎo)入就是一種體現(xiàn))
處理每一個錯誤
創(chuàng)建模塊時的一些經(jīng)驗法則
承諾/生成器/ES6等
Promises:編寫異步代碼的一種方式邪财,它仍然以自頂向下的方式執(zhí)行树埠,并且由于鼓勵使用try / catch樣式錯誤處理而處理更多類型的錯誤
Generators:生成器讓你“暫停”單個函數(shù)又碌,而不會暫停整個程序的狀態(tài),但代碼要稍微復(fù)雜一些铸鹰,以使代碼看起來像自上而下地執(zhí)行
Async functions:異步函數(shù)是一個建議的ES7功能蹋笼,它將以更高級別的語法進(jìn)一步包裝生成器和繼承
什么是“回調(diào)地獄”躁垛?
異步JavaScript或使用回調(diào)的JavaScript很難直觀地得到正確的結(jié)果教馆。很多代碼最終看起來像這樣:
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))
}
})
})
}
})
看到最后的金字塔形狀和所有})土铺?伊克!這被親切地稱為回調(diào)地獄
回調(diào)地獄的原因是镀钓,當(dāng)人們試圖以一種從上到下的視覺方式執(zhí)行JavaScript的方式編寫JavaScript時镀迂。很多人犯這個錯誤探遵!在C妓柜,Ruby或Python等其他語言中棍掐,期望第1行發(fā)生的任何事情都會在第2行的代碼開始運行之前完成,依此類推掘殴。正如你將會學(xué)到的粟誓,JavaScript是不同的
什么是回調(diào)函數(shù)?
回調(diào)只是使用JavaScript函數(shù)的慣例的名稱鹰服。 JavaScript語言中沒有特別的東西叫做“回調(diào)”,它只是一個約定套菜。不像大多數(shù)函數(shù)那樣立即返回一些結(jié)果逗柴,使用回調(diào)函數(shù)需要一些時間來產(chǎn)生結(jié)果。 “異步”這個詞掘而,又名“異步”袍睡,意思是“需要一些時間”或“將來會發(fā)生肋僧,而不是現(xiàn)在”。通持古耍回調(diào)僅在進(jìn)行I / O時使用凭戴,例如下載東西炕矮,閱讀文件肤视,與數(shù)據(jù)庫交互等
當(dāng)你調(diào)用一個普通的函數(shù)時,你可以使用它的返回值
var result = multiplyTwoNumbers(5, 10)
console.log(result // 50 gets printed out
然而腐螟,異步和使用回調(diào)的函數(shù)不會立即返回任何內(nèi)容
var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo is 'undefined'!
在這種情況下乐纸,gif可能需要很長時間才能下載摇予,并且你不希望程序在等待下載完成時暫停()
相反趾盐,你存儲在功能下載完成后應(yīng)運行的代碼小腊。這是回調(diào)秩冈!你把它給到downloadPhoto功能斥扛,它會在下載完成時運行你的回調(diào)(例如'以后再打電話給你')稀颁,并且傳遞照片(或者如果出現(xiàn)錯誤匾灶,會出錯)
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')
人們在嘗試?yán)斫饣卣{(diào)時遇到的最大障礙是理解程序運行時執(zhí)行的順序。在這個例子中發(fā)生了三件事情颊糜。首先聲明handlePhoto函數(shù)衬鱼,然后調(diào)用downloadPhoto函數(shù)并傳遞handlePhoto作為其回調(diào)函數(shù)憔杨,最后打印出“Download started”
請注意消别,handlePhoto尚未被調(diào)用,它只是被創(chuàng)建并作為回調(diào)傳入downloadPhoto。但直到downloadPhoto完成其任務(wù)后才能運行,這可能需要很長時間荆虱,具體取決于Internet連接的速度
這個例子是為了說明兩個重要的概念
- handlePhoto回調(diào)只是稍后存儲一些事情的一種方式
- 事情發(fā)生的順序不是從頂部到底部讀取怀读,而是基于事情完成時跳轉(zhuǎn)
我該如何解決回調(diào)地獄?
回調(diào)地獄是由于糟糕的編碼習(xí)慣造成的骑脱。幸運的是叁丧,編寫更好的代碼并不困難岳瞭! 您只需遵循三條規(guī)則:
1. 保持你的代碼簡短
這里有一些凌亂的瀏覽器JavaScript瞳筏,它使用瀏覽器請求向服務(wù)器發(fā)送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
})
}
這段代碼有兩個匿名函數(shù)姚炕。讓我們給他們的名字
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
})
}
正如你所看到的柱宦,命名函數(shù)非常簡單并且有一些直接的好處
- 由于描述性功能名稱掸刊,使代碼更容易閱讀
- 當(dāng)發(fā)生異常時,你將獲得引用實際函數(shù)名稱而不是“匿名”的堆棧跟蹤
- 允許你移動功能并按名稱引用它們
現(xiàn)在我們可以將這些功能移到我們程序的頂層
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
}
請注意,這里的函數(shù)聲明是在文件底部定義的痒给。這要歸功于提升功能
2. 模塊化
這是最重要的部分:任何人都有能力創(chuàng)建模塊(又名圖書館)骏全。引用(node.js項目的)Isaac Schlueter的話:“編寫一個小模塊姜贡,每個模塊都做一件事,然后將它們組裝成其他模塊熄捍,做更大的事情母怜。如果你不去那里苹熏,你不能進(jìn)入回調(diào)地獄
讓我們從上面取出樣板代碼,并將其分成幾個文件轨域,將其轉(zhuǎn)換為模塊。我將展示一個適用于瀏覽器代碼或服務(wù)器代碼的模塊模式(或者適用于兩者的代碼)
這是一個名為formuploader.js的新文件朱巨,它包含我們之前的兩個函數(shù)
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模塊系統(tǒng)的一個例子枉长,它在node冀续,Electron和使用browserify的瀏覽器中工作琼讽。我非常喜歡這種模式,因為它可以在任何地方工作沥阳,理解起來非常簡單跨琳,并且不需要復(fù)雜的配置文件或腳本
現(xiàn)在我們已經(jīng)有了formuploader.js(并且在瀏覽器中將它作為腳本標(biāo)簽加載到頁面中),我們只需要它并使用它桐罕!以下是我們現(xiàn)在的應(yīng)用程序特定代碼的外觀
var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit
現(xiàn)在我們的應(yīng)用程序只有兩行代碼脉让,并具有以下優(yōu)點:
- 新開發(fā)人員更容易理解 - 他們不會因閱讀所有formuploader函數(shù)而陷入困境
- ormuploader可以在其他地方使用,無需復(fù)制代碼功炮,并且可以輕松地在github或npm上共享
3. 處理每一個錯誤
有不同類型的錯誤:由程序員造成的語法錯誤(通常在你嘗試首次運行程序時發(fā)生),程序員造成的運行時錯誤(代碼已運行但存在導(dǎo)致某些事情混亂的錯誤)薪伏,平臺錯誤由無用的文件權(quán)限滚澜,硬盤驅(qū)動器故障,無網(wǎng)絡(luò)連接等引起的嫁怀。這部分只是為了解決最后一類錯誤
前兩條規(guī)則主要是關(guān)于讓你的代碼可讀设捐,但這是關(guān)于讓代碼穩(wěn)定的。在處理回調(diào)時塘淑,你根據(jù)定義處理已分派的任務(wù)萝招,請在后臺執(zhí)行某些操作,然后成功完成或由于失敗而中止存捺。任何有經(jīng)驗的開發(fā)人員都會告訴你槐沼,你永遠(yuǎn)無法知道這些錯誤何時發(fā)生,所以你必須對它們進(jìn)行計劃
通過回調(diào)捌治,處理錯誤的最常見方法是Node.js樣式岗钩,其中回調(diào)的第一個參數(shù)始終保留用于錯誤
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
}
有第一個參數(shù)是錯誤是一個簡單的慣例,鼓勵你記住處理你的錯誤肖油。如果它是第二個參數(shù)兼吓,你可以編寫像函數(shù)handleFile(file){}的代碼,并且更容易忽略錯誤
代碼庫也可以配置為幫助您記住處理回調(diào)錯誤森枪。最簡單的使用稱為標(biāo)準(zhǔn)视搏。你所要做的就是在你的代碼文件夾中運行$ standard,它會向你顯示你的代碼中的每一個回調(diào)疲恢,并帶有未處理的錯誤
概要
- 不要嵌套功能凶朗。給他們姓名并將他們放在程序的頂層
- 利用函數(shù)提升來利用你的優(yōu)勢來移動函數(shù)
- 處理每個回調(diào)中的每一個錯誤瓷胧。使用標(biāo)準(zhǔn)來幫助你
- 創(chuàng)建可重用的函數(shù)并將它們放在模塊中以減少理解代碼所需的認(rèn)知負(fù)載显拳。將代碼分割成小塊這樣也可以幫助您處理錯誤,編寫測試搓萧,強(qiáng)制您為您的代碼創(chuàng)建穩(wěn)定且文檔化的公共API杂数,并有助于重構(gòu)
避免回調(diào)地獄的最重要的方面是將功能移開宛畦,以便程序流程可以更容易理解,而無需新手參與功能的所有細(xì)節(jié)以了解程序正在嘗試做什么
你可以先將函數(shù)移動到文件底部揍移,然后使用require('./ photo-helpers.js')等相關(guān)需求將它們移動到另一個文件中次和,然后將它們移動到獨立模塊like require('image-resize'))
以下是創(chuàng)建模塊時的一些經(jīng)驗法則:
- 首先將重復(fù)使用的代碼移入一個函數(shù)
- 當(dāng)你的函數(shù)(或與同一主題相關(guān)的一組函數(shù))變得足夠大時,將它們移動到另一個文件中并使用module.exports將其公開那伐。你可以使用相對需求來加載它
- 如果你有一些可以在多個項目中使用的代碼踏施,給它自己的readme,tests和package.json罕邀,并將它發(fā)布到github和npm畅形。這里列出的具體方法有太多令人敬畏的好處
- 一個好的模塊很小,專注于一個問題
- 模塊中的單個文件不應(yīng)超過150行左右的JavaScript
- 一個模塊不應(yīng)該有多于一個嵌套文件夾級別的文件夾诉探。如果是這樣日熬,它可能做了太多事情
- 請你認(rèn)識的更有經(jīng)驗的編程人員向你展示優(yōu)秀模塊的例子,直到你對他們的樣子有了一個好的想法肾胯。如果需要花費幾分鐘時間才能了解正在發(fā)生的事情竖席,那么它可能不是一個很好的模塊
承諾/生成器/ES6等呢
在研究更先進(jìn)的解決方案之前,請記住敬肚,回調(diào)是JavaScript的基本組成部分(因為它們只是函數(shù))毕荐,你應(yīng)該在學(xué)習(xí)更先進(jìn)的語言特性之前學(xué)習(xí)如何讀寫它們,因為它們都依賴于對回調(diào)帘皿。如果你還不能編寫可維護(hù)的回調(diào)代碼东跪,請繼續(xù)使用它
如果你真的希望你的異步代碼從頭到尾閱讀,你可以嘗試一些奇特的東西鹰溜。請注意虽填,這些可能會引入性能和/或跨平臺運行時兼容性問題
- Promises:是編寫異步代碼的一種方式,它仍然以自頂向下的方式執(zhí)行曹动,并且由于鼓勵使用try / catch樣式錯誤處理而處理更多類型的錯誤
- Generators生成器讓你“暫驼眨”單個函數(shù),而不會暫停整個程序的狀態(tài)墓陈,但代碼要稍微復(fù)雜一些恶守,以使代碼看起來像自上而下地執(zhí)行。
- Async functions異步函數(shù)是一個建議的ES7功能贡必,它將以更高級別的語法進(jìn)一步包裝生成器和承諾兔港。
總結(jié)
回調(diào)地獄最主要的就是因為功能邏輯代碼嵌套的層次太多,導(dǎo)致可讀性降低,維護(hù)困難,避免回調(diào)地獄的最重要的方面是將功能移開,保持代碼簡單,不嵌套并分成小模塊,也就是多多進(jìn)行代碼封裝,將你所要的屬性和方法用function關(guān)鍵字包裹起來,而且還要給它取一個有意義的名字,例如:頁面上彈框,顯示,隱藏,下拉等各個功能小模塊,分別用有名函數(shù)給包裹起來,少用匿名函數(shù),以便可以重復(fù)的多次使用,這也是可以便于程序流程的理解
除了常見的一種回調(diào)函數(shù)作為異步處理,還有promises,Generators,async是處理異步處理的方式,,關(guān)于這三個我也在學(xué)習(xí)當(dāng)中,理論的東西雖是概念,沒有大量代碼的編寫,個人覺得是很難理解這些東西,但是代碼就是這些語言文字實實在在的轉(zhuǎn)化,騷年們,加油,加油....