为了解决这个阻塞问题,JavaScript严重依赖于回调,这是在长时间运行的进程(IO,定时器等)完成后运行的函数,因此允许代码执行经过长时间运行的任务。
downloadFile('example.com/weather.json', function(err, data) { console.log('Got weather data:', data); });
但是,问题来了,回调地狱
虽然回调的概念在理论上是巨大的,但它可能导致一些真正令人困惑和难以阅读的代码。 想象一下,如果你需要在回调后进行回调:
getData(function(a){ getMoreData(a, function(b){ getMoreData(b, function(c){ getMoreData(c, function(d){ getMoreData(d, function(e){ ... }); }); }); }); });
你可以看到,这真的是一发不可收拾。 抛出一些if语句,for循环,函数调用或注释,你会有一些非常难读的代码。 初学者特别是这个的受害者,不理解如何避免这个“金字塔的厄运”。
这种层层嵌套的代码给开发带来了很多问题,主要体现在:
1.代码可能性变差
2.调试困难
3.出现异常后难以排查
解决方案
design around it
因此,许多程序员都陷入了回调地狱,因为这个(糟糕的设计)。 他们并没有真正考虑他们的代码结构提前,没有意识到他们的代码有多糟糕,意识到已经太晚了。 和你写的任何代码一样,你应该停下来思考可以做什么,使它在编写它之前或之后更简单,更可读。 这里有几个提示,你可以用来避免回调地狱(或至少管理它)。
Give your functions names
当读取代码(特别是乱码,无组织的代码)时,它很容易失去逻辑流,甚至语法,当小空间拥塞这么多嵌套回调。 帮助打击这一点的一个方法是命名你的函数,所以你需要做的是看一下名字,你会有一个更好的主意,它做什么。 它也给你的眼睛一个语法参考点。
考虑下面的代码:
var fs = require('fs'); var myFile = '/tmp/test'; fs.readFile(myFile, 'utf8', function(err, txt) { if (err) return console.log(err); txt = txt + ' Appended something!'; fs.writeFile(myFile, txt, function(err) { if(err) return console.log(err); console.log('Appended text!'); }); });
看看这可能需要几秒钟来实现每个回调的作用和它开始的地方。 向函数中添加一些额外的信息(名称)会对可读性产生重大影响,尤其是在回调中有多个级别时:
var fs = require('fs'); var myFile = '/tmp/test'; fs.readFile(myFile, 'utf8', function appendText(err, txt) { if (err) return console.log(err); txt = txt + ' Appended something!'; fs.writeFile(myFile, txt, function notifyUser(err) { if(err) return console.log(err); console.log('Appended text!'); }); });
现在只是快速浏览会告诉你第一个函数附加一些文本,而第二个函数通知用户的变化。
Declare your functions beforehand
减少代码杂乱的最好方法之一是保持更好的代码分离。 如果你事先声明一个回调函数并稍后调用它,你将避免深层嵌套的结构,这使得回调很难使用。
So you could go from this...
var fs = require('fs'); var myFile = '/tmp/test'; fs.readFile(myFile, 'utf8', function(err, txt) { if (err) return console.log(err); txt = txt + ' Appended something!'; fs.writeFile(myFile, txt, function(err) { if(err) return console.log(err); console.log('Appended text!'); }); });
...to this:
var fs = require('fs'); function notifyUser(err) { if(err) return console.log(err); console.log('Appended text!'); }; function appendText(err, txt) { if (err) return console.log(err); txt = txt + ' Appended something!'; fs.writeFile(myFile, txt, notifyUser); } var myFile = '/tmp/test'; fs.readFile(myFile, 'utf8', appendText);
虽然这可以是一个很好的方式来帮助缓解问题,但它并不能完全解决问题。 当读取以这种方式编写的代码时,如果你不记得每个函数的确切位置,那么你必须回去查看每个函数,以回溯逻辑流程,这可能需要时间。
Use modules
在几乎每种编程语言中,降低复杂性的最好方法之一是模块化。 JavaScript也不例外。 每当你编写代码时,花一些时间来回顾一下你是否经常遇到一个常见的模式。
你在不同的地方多次写相同的代码吗? 你的代码的不同部分是否遵循一个共同的主题? 如果是这样,你有机会清理东西,抽象和重用代码。
有数千个模块,你可以看看供参考,但这里有几个要考虑。 它们处理常见的,但非常具体的任务,否则会扰乱你的代码并降低可读性:Pluralize,csv,qs,clone。
Here is a new file called formuploader.js
that contains our two functions from before:
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 }
Now that we have formuploader.js
(and it is loaded in the page as a script tag after being browserified) we just need to require it and use it! Here is how our application specific code looks now:
var formUploader = require('formuploader') document.querySelector('form').onsubmit = formUploader.submit
Async.js
幸运的是,像Async.js这样的库存在尝试和遏制这个问题。 Async在你的代码之上添加了一层函数,但可以通过避免回调嵌套大大降低复杂性。
许多辅助方法存在于Async中,可以在不同的情况下使用,例如系列,并行,瀑布等。每个函数都有一个特定的用例,所以花一些时间来了解哪个在哪些情况下会有帮助。
与Async一样好,像什么,它不完美。 它很容易结合series, parallel,waterfall, forever, etc,在这一点你回到你开始与凌乱的代码。 注意不要过早优化。 只是因为一些异步任务可以并行运行并不总是意味着他们应该。 实际上,由于Node只有单线程,因此使用Async时并行运行任务的性能增益很少甚至没有。
上面的代码可以使用Async的瀑布简化:(前一个函数的回调会作为后一个函数的参数,如果有任何任务通过一个错误的回调,下一个函数不执行)
var fs = require('fs'); var async = require('async'); var myFile = '/tmp/test'; async.waterfall([ function(callback) { fs.readFile(myFile, 'utf8', txt); }, function(txt, callback) { txt = txt + ' Appended something!'; fs.writeFile(myFile, txt, callback); } ], function (err, result) { if(err) return console.log(err); console.log('Appended text!'); });
Promises
虽然Promises可以花费一些时间来掌握,但在我看来,它们是您可以在JavaScript中学习的更重要的概念之一。 它不仅大大减少了代码行数,而且使代码的逻辑流程更容易遵循。
这里是一个使用非常快,非常受欢迎的Promise库,Bluebird的例子:
var Promise = require('bluebird'); var fs = require('fs'); Promise.promisifyAll(fs); var myFile = '/tmp/test'; fs.readFileAsync(myFile, 'utf8').then(function(txt) { txt = txt + ' Appended something!'; fs.writeFile(myFile, txt); }).then(function() { console.log('Appended text!'); }).catch(function(err) { console.log(err); });
请注意,这个解决方案不仅比以前的解决方案更短,而且更容易阅读(尽管,诚然,Promise风格的代码可能需要一些习惯)。 花时间学习和理解承诺,这将是值得你的时间。 但是,Promise绝对不是解决我们在异步编程中的所有问题,所以不要假设通过使用它们,你会有一个快速,干净,无bug的应用程序。 关键是知道什么时候对你有用。
一些Promise库你应该检查是Q,bluebird,或内置Promises如果你使用ES6的话。
Async/Await
注意:这是一个ES7功能,目前不支持Node或io.js。 但是,你现在可以使用它像Babel一样的转换器。
清除代码的另一个选项是我最近喜欢的(当它有更广泛的支持时),它使用异步函数。 这将允许你编写看起来更像同步代码,但仍然是异步的代码。
An example:
async function getUser(id) { if (id) { return await db.user.byId(id); } else { throw 'Invalid ID!'; } } try { let user = await getUser(123); } catch(err) { console.error(err); }
The db.user.byId(id)
call returns a Promise
, which we'd normally have to use with .then()
, but with await
we can return the resolved value directly.
Notice that the function containing the await
call is prefixed with async
, which tells us that it contains asynchronous code and must also be called with await
.
Another big advantage to this method is we can now use try/catch
, for
, and while
with our asynchronous functions, which is much more intuitive than chaining promises together.
Aside from using transpilers like Babel and Traceur, you can also get functionality like this in Node with the asyncawait package.
避免这样的常见问题,如回调地狱不容易,所以不要期待立即结束你的挫折。 我们都陷入了它。 只是尝试减慢,花一些时间来思考你的代码的结构。 像任何事情,实践使完美。