日期:2014-05-16  浏览次数:20371 次

我们为什么要使用NodeJS
        科普文一则,说说我对NodeJS(一种服务端JavaScript实现)的一些认识,以及我为什么会向后端工程师推荐NodeJS.

        "Node.js 是服务器端的 JavaScript 运行环境,它具有无阻塞(non-blocking)和事件驱动(event-driven)等的特色,Node.js 采用 V8 引擎,同样,Node.js 实现了类似 Apache 和 nginx 的web服务,让你可以通过它来搭建基于 JavaScript 的 Web App。"

        上周末参与了CNodeJS社区的第一次北京聚会,现场气氛非常的好.而作为一名前端开发,我在后面的讨论环节讲了下我对NodeJS的看法,主要回答的问题是"我为什么会向后端工程师推荐NodeJS".这其实是去年年底大团队技术总结的话题之一,包含在我之前发过的PPT:团队年终技术Review中.因为之前没有准备,当天仓促上阵,也不知道说清楚了没,不如就在这里再详细展开记录下.

        我想不仅仅是NodeJS,当我们要引入任何一种新技术前都必须要搞清楚几个问题:
        1.我们遇到了什么问题?
        2.这项新技术解决什么问题,是否契合我们遇到的问题?
        3.我们遇到问题的多种解决方案中,当前这项新技术的优势体现在哪儿?
        4.使用新技术,带来哪些新问题,严重么,我们能否解决掉?

我们的问题:Server端阻塞
        NodeJS被设计用来解决服务端阻塞问题.通过一段简单的代码解释何为阻塞:
//根据ID,在数据库中Persons表中查出Name
var name = db.query("select name from persons where id=1");
//进程等待数据查询完毕,然后使用查询结果.
output("name")

        这段代码的问题是在上面两个语句之间,在整个数据查询的过程中,当前程序进程往往只是在等待结果的返回.这就造成了进程的阻塞.对于高并发,I/O密集行的网络应用中,一方面进程很长时间处于等待状态,一方面为了应付新的请求不断的增加新的进程.这样的浪费会导致系统支持QPS远远小于后端数据服务能够支撑的QPS,成为系统的瓶颈.而且这样的系统也特别容易被慢链接攻击(客户端故意不接收或减缓接收数据,加长进程等待时间).
如何解决阻塞问题
        解决这个问题的办法是,建立一种事件机制,发起查询请求之后,立即将进程交出,当数据返回后触发事件,再继续处理数据:
//定义如何后续数据处理函数
function onDataLoad(name){
    output("name");
}
//发起数据请求,同时指定数据返回后的回调函数
db.query("select name from persons where id=1",onDataLoad);

        我们看到按照这个思路解决阻塞问题,首先我们要提供一套高效的异步事件调度机制.而主要用于处理浏览器端的各种交互事件的JavaScript.相对于其他语言,至少有两个关键点特别适合完成这个任务.
为什么JS适合解决阻塞问题
        首先JavaScript是一种函数式编程语言,函数编程语言最重要的数学基础是λ演算(lambda calculus) -- 即函数可以接受函数当作输入(参数)和输出(返回值).
        函数可以作为其他函数的参数输入的这个特性,使得为事件指定回调函数变得很容易.特别是JavaScript还支持匿名函数.通过匿名函数的辅助,之前的代码可以进行简写如下.
db.query("select name from persons where id=1",function(name){
    output(name);
});

        还有一个关键问题是,异步回调的运行上下文保持(称状态保持),我看一段代码来说明何为状态保持.
//传统同步写法:将查询和结果打印抽象为一个方法
function main(){
    var id = "1";
    var name = db.query("select name from persons where id=" + id);
    output("person id:" + id + ", name:" + name);
}
main();

        前面的写法在传统的阻塞是编程中非常常见,但接下来进行异步改写时会遇到一些困扰.
//异步写法:
function main(){
    var id = "1";
    db.query("select name from persons where id=" + id,function(name){
        output("person id:" + id + ", name:" + name);//n秒后数据返回后执行回调
    });
}
main();

        细心的同学可以注意到,当等待了n秒数据查询结果返回后执行回调时.回调函数中却仍然使用了main函数的局部变量"id",而"id"已经在n秒前走出了其作用域,这是为什么呢?熟悉JavaScript的同学会淡然告诉您:"这是闭包(closures)~".
        其实在复杂的应用中,我们一定会遇到这类场景.即在函数运行时需要访问函数定义时的上下文数据(注意:一定要区分函数定义时和函数运行时这样的字眼和其代表的意义,不然很快就会糊涂).而在异步编程中,函数的定义和运行又分处不同的时间段,那么保持上下文的问题变得更加突出了.
        在这个例子中,db.query作为一个公共的数据库查询方法,把"id"这个业务数据传入给db.query,交由其保存是不太合适的.但聪明的同学们可以抽象一下,让db.query再支持一个需要保持状态的数据对象传入,当数据查询完毕后可以把这些状态数据原封不动的回传.如下:
function main(){
    var id = "1";
    var currentState = new Object();
    currentState.person_id = id;
    db.query("select name from persons where id=" + id, function(name,state){
        output("person id:" + state.person_id + ", name:" + name);
    },currentState);//注意currentState是db.query的第三个参数
}
main();

&