Node.js入门 - 回调函数
如果你还没读过第一篇,先跑到这里去瞧一瞧,我保证你多花这么一点时间是值得的。
到目前为止,我们已经学会了用Node.js做一些基本的事(比如用来打印金光闪闪的Hello World), 接下来我们将去学习一下回调函数并了解为什么这玩意是如此的有用。
为什么是Node.js?
现在已经有很多编程语言,并且各有各的优缺点。能够对比不同编程技术,并选出最佳编程语言来解决问题,是创建一个可靠的产品的重要前提条件。跟所有别的编程语言一样,Node.js有它自身的优点和缺点,那么让我们来看一下应该在什么情况下使用Node.js吧。
优点:
- a) Node.js用于解决高并发带来的瓶颈。比如你要创建一个需要短时间内处理成千上万的请求,那么Node.js无疑是一个很棒的选择。
- b) 这就是JavaScript,这意味着任何有前端JavaScript编程经验的人都可以把以前的经验带到Node.js编程里来,并且它是基于Google的V8引擎,速度极快。
- c) 如果你在客户端和服务器端同时使用JavaScript,这会让你的编程变得轻松和快速。比如:你如果要做一些校验,服务器端和客户端可以共用一样的代码。而前后端数据结构一致,数据传输时超容易。
- d) 你会让那些做PHP的同志们惊讶得下巴掉下来。(这哥们黑PHP可真是不遗余力啊。)
光说好的也不行对吧,让我们来看一下不好的。
缺点:
- a) 这是一门新技术,开发者们还来不及创建健壮的测试模型,也还没有好用的IDE出现。并且当出现问题时,能找到的现成解决方案和文档要比像PHP一样的技术少得多。虽然在当下这是一个劣势,但随着这门技术越来越流行,相信假以时日,就会积累够多的资源可以快速轻松地开发。
- b) 代码容易写的混乱(JavaScript的先天属性),要让Node.js程序很好地运行,要用很多匿名函数和回调,而回调函数的一大缺点就是容易让你的逻辑被打得支离破碎,程序越大,给阅读和调试带来的麻烦就越多。
- c) 不能很好地处理数字(不确定这句话的意思,原文是It's not the best number cruncher), 可是PHP或者Ruby同样有这个问题!(这次把Ruby也带上一起黑了)。
了解Node.js和优势和劣势这点很重要,这才能让你决定Node.js是不是满足你的需求的最佳解决方案。不过要是你的代码像狗屎一样,你选啥技术也只会写出狗屎一样的程序来,这已经不是技术方案优劣的讨论范畴了。在学习回调之前,让我们先快速了解一下阻塞(blocking)和非阻塞(none-blocking)模型,以及对于我们代码性能的影响。
阻塞和非阻塞(Blocking and none-blocking)
阻塞指的是因为别的东西阻止你当前代码的执行,通常发生在你的应用在等待其它资源,比如CPU,网络,内存和硬盘等。为了更好地解释阻塞,我们来看一下以下代码:
var http = require('http');
var url = require('url');
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
if( url.parse(request.url).pathname == '/wait' ){
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + 15000);
response.write('Thanks for waiting!');
} else{
response.write('Hello!');
}
response.end();
}).listen(8080);
console.log('Server started');
把文件保存为"blocking.js",并在终端中执行命令"node blocking.js"。
现在我们运行了一个阻塞脚本,当用户访问'/wait'的时候,将运行一个循环,15秒之后打出一句话"Thanks for waiting!"。先打开浏览器,在地址栏输入http://localhost:8080/wait,然后再直接访问http://localhost:8080(记住时间尽量短,不要超过15秒哦)。这时候你应该发现即使是切换到了第二个url,但是牛B的"Hello"还是要等那15秒执行完毕后才打出来,这是肿么了?
Node.js被设计成单线程执行模型,这让它的表现异于其它Web开发技术,比如PHP会为每个连接创建一个新的线程。而在Node.js里,如果前一个请求需要耗时5秒钟,则后一个请求必须等待5秒后才能执行。我们把这个叫“阻塞”,前一个线程“阻塞”了后一个线程。
这通常在多数Web场景中是不可接受的,那怎么来避免“阻塞”呢?首先我们要让自己的代码变成“非阻塞”的,为了实现这个功能我们要用到回调函数。
什么是回调?
如果你以前有过使用JavaScript的经验,你应该对回调函数已经有所了解。让我们来想像一个基本场景,我们要干某件耗费大量时间的事,比如试图读取一个大文件,我们不想让Node.js服务必须读完这个文件才能响应别的请求,为了完成这样的功能,我们最好是告诉Node.js在后台读取这个文件,当读取完之后通过某种方式通知我们,而在这个过程中, 服务器同时还能处理别的请求,也就是使我们的代码变成“非阻塞”,就像我们刚刚举的例子一样,以适用于同时时间处理多个请求的情况,我们也把这个叫做异步调用。
为了更清楚说明"非阻塞","异步"是怎么工作的,我们来看一下下面这个例子:
你在一个狭窄的道路上开车,前面有一个SB在停着打电话,很忙的样子(阻塞代码)使你不能到达目的地,这样你必须等这个SB打完电话把车启动起来才能继续(有人可能想,用板砖干他丫的,但从程序角度来说,把他丫干死,前面少了一个司机,你要等警察来拖走或者自己先开走他的后再开自己的车,外加法律责任,代价是很大滴,这叫破坏模型,比阻塞模型的代价还大)。
想像一下如果这条路上有紧急停车带,前面那SB司机可以变得不SB,先把车停在紧急停车带打电话。把路让出来让你先继续你的旅程。当那个不再SB的司机打完电话之后也可以回到主干上来继续前行,还避免了可能碰到的板砖型程序员而导致血光之灾,皆大欢喜。这跟异步调用很像,在同一时间同一主干上跑多辆车。
好了,现在用我们新学到的知识来写"非阻塞"代码吧,还是实现刚才那个功能。
首先放一段阻塞代码在一个新文件里:
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + 10000);
保存为"block.js"。
接下来创建服务:
var http = require('http');
var url = require('url');
var cp = require('child_process');
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
if( pathname == '/wait' ){
cp.exec('node block.js', myCallback);
}
else{
response.writeHead(200, {'Content-Type': 'text/plain'});
response.write('Hello!\n');
response.end();
}
console.log('New connection');
function myCallback(){
response.writeHead(200, {'Content-Type': 'text/plain'});
response.write('Thanks for waiting!\n');
response.end();
}
}
http.createServer(onRequest).listen(8080);
console.log('Server started');
这里我们使用了"子线程"模块来调用新的线程,也就是阻塞程序block.js,而我们的主线程则可以高高兴兴地处理别的请求。我们调用子线程模块的.exec函数启动阻塞线程并运行之,.exec()函数有两个参数第一个是调用block.js的node命令,第二个是回调函数。当.exec()执行完毕之后会调用回调函数myCallback打印"Thanks for waiting!"。
把上面的代码保存为"nonblocking.js",然后执行"node nonblocking.js"测试。
现在你应该注意到,在先访问http://localhost:8080/wait 后访问http://localhost:8080这个过程中,后者不再等待前者15秒了。很明显这又是一个没啥意思的程序,"Thanks for waiting!"这几个字并不比"Hello World!"要有趣多少。那么,让我们利用上述结构的程序来写一个稍微有那么一点意思的程序吧。
幸运的是Node.js内置很多非阻塞的异步回调以供我们在应用程序中方便地使用,下面是一个非阻塞文件读取器的代码:
var http = require('http');
var fileSystem = require('fs');
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
var read_stream = fileSystem.createReadStream('myfile.txt');
read_stream.on('data', writeCallback);
read_stream.on('close', closeCallback);
function writeCallback(data){
response.write(data);
}
function closeCallback(){
response.end();
}
}).listen(8080);
console.log('Server started');
这里我们使用了Node.js内置的文件系统模块,用.createReadStream()读取我们的文件,并绑定两个函数到"data"和"close"事件。这些函数将在事件引发时执行。
把上述代码存为"fileReader.js",在同级目录下新建一个文本文件"myfile.txt", 执行"node fileReader.js"命令,现在你可以在http://localhost:8080 看到浏览器里显示了myfile.txt内容了。
总结
无论你是否有需要执行一个耗时很长的程序与否,你都应该使用非阻塞模型,并且记住,正确使用回调和异步可以让代码的速度和稳定性都能得到提高。