JavaScript異步程序書寫指南javascript
咱們很難一眼就看懂異步JavaScript,或者是使用回調函數的JavaScript程序。例以下面這段代碼:java
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)) } }) }) } })
這個一堆以})
結尾的金字塔,咱們很親切地稱它爲——「回調地獄」。node
之因此會出現回調地獄,是由於咱們寫JavaScript通常是視覺上的從上到下書寫。不少人犯了這個錯誤!在例如C、Ruby或者Python等其餘語言,在第二行代碼運行以前,第一行代碼確定已經運行完了。然而如後面所說的,JavaScript是不一樣的。git
回調函數是JavaScript里約定俗成的一個名稱。實際上並不存在肯定的「回調函數」,只是你們就管那個位置的函數做回調函數。與大多數運行後馬上給出結果的函數不一樣,使用回調的函數要花一些時間才能得出結果。「異步」這個詞就是表明‘要花時間,未來運行’。一般回調函數會用在下載文件、讀取文件、或者數據庫相關事務等。程序員
當你調用一個普通函數,你能夠馬上獲得它的值:github
var result = multiplyTwoNumbers(5, 10) console.log(result) // 50 gets printed out
而使用回調的函數不能馬上獲得反饋。數據庫
var photo = downloadPhoto('http://coolcats.com/cat.gif') // photo is 'undefined'!
這個時候,這張gif可能要下載好久,你總不能讓程序什麼都不幹停下來就等它下載完。npm
相反,你能夠儲存下載完後觸發的代碼到一個函數裏,這就是回調函數!把這些代碼寫進downloadPhoto
函數,下載成功後,會運行回調函數。json
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')
咱們理解回調最難的地方就是理解程序的運行順序。例子中發生了三個主要事件,首先是handlePhoto
函數被聲明,而後做爲回調函數被downloadPhoto
函數調用,最後控制檯打印出'Download started'
。promise
注意handlePhoto
尚未被調用,它只是被建立而後最爲回調函數傳入downloadPhoto
。直到downloadPhoto
完成下載,他都不會運行。
這個例子說明兩個問題:
handlePhoto
(回調函數)只是儲存了將要運行的東西
不要從上到下閱讀程序,程序會根據事情完成而跳轉
你只須要跟着一下三步走:
如下是一些用於AJAX的瀏覽器端代碼(使用browser-request):
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 = 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 }
注意,函數聲明在底部,卻仍然能調用,這得益於函數提高。
用上面的例子,咱們將把它拆分紅多個文件,我會告訴你怎麼把他作成模塊。
建立一個包含前面兩個函數的新文件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的模塊系統,可使用在node、Electron,瀏覽器上(藉助browserify)。我十分喜歡這種風格,由於哪兒都能用,並且易於理解,不用依賴於其餘複雜設置。
咱們獲得了formuploader.js
,只要引入並使用它就能夠了!操做以下:
var formUploader = require('formuploader') document.querySelector('form').onsubmit = formUploader.submit
如今咱們的代碼只有兩行,有如下好處:
易於新開發者理解,他們不會爲讀取全部的formuploader
函數而陷入困境。
formuploader
不用複製粘貼代碼,只要在github或者npm下載分享的代碼就能夠了。
常見錯誤有幾種
語法錯誤(運行失敗)
運行時錯誤(能夠運行可是有bug)
平臺錯誤(文件權限問題、磁盤問題、網絡問題)
前兩條規則主要是提升你的代碼的可讀性,而這條是讓你的代碼更穩定。在處理回調時,您將根據定義處理髮送的任務,在後臺執行某些操做,最後成功完成或失敗停止。任何有經驗的開發人員都會告訴你,你永遠不會知道這些錯誤發生何時發生,因此在問題出現時都必須有所對策。
最經常使用的回調錯誤處理是Node.js風格,也就是回調函數的第一個參數老是錯誤參數。
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
是一個簡單的共識,這樣作能夠提醒你必須處理你的錯誤。若是是第二個參數的話你很容易把代碼寫成function handleFile (file) { }
而後就忘了處理錯誤。
代碼規範化工具也能夠提醒你添加回調錯誤處理,最簡單的方法之一是使用standard。只是在你的文件目錄運行 $ standard
就能檢查你的代碼有沒有缺乏錯誤處理。
不要嵌套函數,命名後調用更好
使用函數提高
處理回調函數的每個錯誤
建立可重用函數,寫成模塊,讓你更容易讀懂代碼。把你的代碼拆分紅小塊能夠幫助你處理錯誤,寫測試,重構,方便爲你的代碼寫更穩定的API
避免回調地獄的最重要的是移動函數,以便程序流程能夠更容易地被理解,其餘程序員能夠不翻遍整個文件就能知道這段程序的功能。
你能夠先把函數移動到底部,而後逐漸把函數寫到模塊文件裏,而後使用require
引入它(就像引用其餘npm模塊同樣)。
一些寫模塊的經驗:
先把常常重複使用的功能寫成一個函數
當這個函數寫得夠大以後,把他移動到另外一個文件,用module.exports
暴露它,而後用require
引入
若是你的代碼是通用的,能夠寫readme文件和package.json
發佈到npm或者github
一個好模塊,體積要小,並且針對只一個問題
模塊中的單個文件不該超過約150行
模塊不該該有多個級別的嵌套文件夾,其中包含JavaScript文件。若是是這樣,它可能作的太多了
讓有經驗的程序員介紹你一些好用的模塊,嘗試理解這個模塊的功能,若是花了幾分鐘的話,這個模塊可能就不夠好了
在查看更高級的解決方案以前,請記住,回調是JavaScript的一個基本部分(由於它們只是函數),你應該學習如何讀寫它們,而後再轉向更高級的語言功能,由於它們依賴於對回調的理解。若是您還不能寫可維護的回調代碼,請繼續努力學習!
若是你真的想你的異步代碼能夠「從上至下閱讀」,你能夠試試這些美妙的方法。注意,這些功能在不一樣平臺會有兼容性問題,使用前請先調查清楚!
Promise就是一種讓你從上至下寫回調函數的方法,它鼓勵你使用try/catch處理更多類型的錯誤。
Generator可讓你「暫停」一個函數(而不暫停整個程序),它也能你從上至下寫異步函數,可是代價是代碼有點複雜難以理解。wat就是使用這個方法。
Async functions是ES7的特性,是生成器和promise更高級的封裝,有興趣本身谷歌一下唄。
就我我的而言,我使用回調函數處理90%的異步代碼,當事情變得複雜時,依靠一些庫,例如run-parallel或者run-series。我不認爲研究回調 vs promise vs 其餘什麼方法對我來講有什麼幫助,最重要的仍是保持代碼簡單,不嵌套,並分紅小模塊。
不管你選擇何種方法,請始終處理每一個錯誤,並保持代碼簡潔。
原文: http://callbackhell.com/
本文github地址: https://github.com/ssshooter/...