回調(diào)地獄

如果你想閱讀體驗更好,可以戳鏈接回調(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)疲恢,并帶有未處理的錯誤

概要

  1. 不要嵌套功能凶朗。給他們姓名并將他們放在程序的頂層
  2. 利用函數(shù)提升來利用你的優(yōu)勢來移動函數(shù)
  3. 處理每個回調(diào)中的每一個錯誤瓷胧。使用標(biāo)準(zhǔn)來幫助你
  4. 創(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)化,騷年們,加油,加油....

原文閱讀出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仔拟,一起剝皮案震驚了整個濱河市衫樊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖科侈,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件载佳,死亡現(xiàn)場離奇詭異,居然都是意外死亡臀栈,警方通過查閱死者的電腦和手機(jī)蔫慧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來权薯,“玉大人姑躲,你說我怎么就攤上這事∶蓑迹” “怎么了肋联?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長刁俭。 經(jīng)常有香客問我橄仍,道長,這世上最難降的妖魔是什么牍戚? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任侮繁,我火速辦了婚禮,結(jié)果婚禮上如孝,老公的妹妹穿的比我還像新娘宪哩。我一直安慰自己,他們只是感情好第晰,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布锁孟。 她就那樣靜靜地躺著,像睡著了一般茁瘦。 火紅的嫁衣襯著肌膚如雪品抽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天甜熔,我揣著相機(jī)與錄音圆恤,去河邊找鬼。 笑死腔稀,一個胖子當(dāng)著我的面吹牛盆昙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播焊虏,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼淡喜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了诵闭?” 一聲冷哼從身側(cè)響起炼团,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后们镜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡润歉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年模狭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片踩衩。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡嚼鹉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出驱富,到底是詐尸還是另有隱情锚赤,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布褐鸥,位于F島的核電站线脚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏叫榕。R本人自食惡果不足惜浑侥,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望晰绎。 院中可真熱鬧寓落,春花似錦、人聲如沸荞下。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尖昏。三九已至仰税,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抽诉,已是汗流浹背肖卧。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留掸鹅,地道東北人塞帐。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像巍沙,于是被迫代替她去往敵國和親葵姥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 不知不覺間已經(jīng)初三開學(xué)倆月了句携,從大夏天炎炎酷日到轉(zhuǎn)眼間落葉灰黃榔幸,似乎不過是一個愣神。 初三是個神奇的幻想。我一直是...
    言皙閱讀 1,102評論 0 6
  • 2015-04-24 大家都不容易 請互相體諒吧 外婆今年過年的時候削咆,不小心把假牙磕著了牍疏。總盤算著要去醫(yī)院換假牙拨齐,...
    一時佛在閱讀 4,230評論 0 5
  • 在《自愈的本能》這本書里有這樣的一段話:戈特曼博士將引發(fā)糟糕局面的態(tài)度描述為“天啟四騎士”鳞陨,即四種會在人一生當(dāng)中的...
    經(jīng)久不衰妮妮閱讀 151評論 0 0
  • 凌晨四五點 半夢半醒間 耳旁傳來 “日落,又日出 一個人 走兩個人的路” 曉夢之境浮現(xiàn)腦海 詞瞻惋、夢相接處 增淚痕 ...
    旅行昊昊閱讀 467評論 1 0