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

我为什么向后端工程师推荐Node.js(转)
我为什么向后端工程师推荐Node.js

科普文一则,说说我对Node.js的一些认识,以及我作为前端工程师为什么会向后端工程师推荐Node.js。

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

我想不仅仅是Node.js,当我们要引入任何一种新技术前都必须要搞清楚几个问题:

    我们遇到了什么问题?
    这项新技术解决什么问题,是否契合我们遇到的问题?
    我们遇到问题的多种解决方案中,当前这项新技术的优势体现在哪儿?
    使用新技术,带来哪些新问题,严重么,我们能否解决掉?

我们的问题:Server端阻塞

Node.js被设计用来解决服务端阻塞问题.下面通过一段简单的代码解释何为阻塞:

Js代码:

//根据ID,在数据库中Persons表中查出Name
var name = db.query("select name from persons where id=1");
//进程等待数据查询完毕,然后使用查询结果。
output("name")

这段代码的问题是在上面两个语句之间,在整个数据查询的过程中,当前程序进程往往只是在等待结果的返回.这就造成了进程的阻塞.对于高并发,I/O密集行的网络应用中,一方面进程很长时间处于等待状态,另一方面为了应付新的请求不断的增加新的进程.这样的浪费会导致系统支持QPS远远小于后端数据服务能够支撑的QPS,成为了系统的瓶颈.而且这样的系统也特别容易被慢链接攻击(客户端故意不接收或减缓接收数据,加长进程等待时间)。
如何解决阻塞问题

可以引入事件处理机制解决这个问题。在查询请求发起之前注册数据加载事件的响应函数,请求发出之后立即将进程交出,而当数据返回后再触发这个事件并在预定好的事件响应函数中继续处理数据:

Js代码:

//定义如何后续数据处理函数
function onDataLoad(name){
   output("name");
}
//发起数据请求,同时指定数据返回后的回调函数
db.query("select name from persons where id=1",onDataLoad);

我们看到若按照这个思路解决阻塞问题,首先我们要提供一套高效的异步事件调度机制.而主要用于处理浏览器端的各种交互事件的JavaScript。相对于其他语言,至少有两个关键点特别适合完成这个任务。
为什么JS适合解决阻塞问题

首先JavaScript是一种函数式编程语言,函数编程语言最重要的数学基础是λ演算(lambda calculus) -- 即函数对象可以作为其他函数对象的输入(参数)和输出(返回值)。

这个特性使得为事件指定回调函数变得很容易。特别是JavaScript还支持匿名函数。通过匿名函数的辅助,之前的代码可以进行简写如下:

Js代码:

db.query("select name from persons where id=1",function(name){
    output(name);
});

还有另一个关键问题是,异步回调的运行上下文保持(本文暂称其为"状态保持")。我们先来看一段代码来说明何为状态保持:

Js代码:

//传统同步写法:将查询和结果打印抽象为一个方法
function main(){     
    var id = "1";     
    var name = db.query("select name from persons where id=" + id);     
    output("person id:" + id + ", name:" + name); 
}
main();

前面的写法在传统的阻塞是编程中非常常见,但接下来进行异步改写时会遇到一些困扰:

Js代码:

//异步写法:
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秒前走出其作用域。为什么此时"id"仍然可以访问呢,这是因为JavaScript的另外一个重要语言特性:闭包(Closures)。接下来我来详解闭包的原委。

在复杂的应用中,我们一定会遇到这类场景。即在函数运行时需要访问函数定义时的上下文数据(注意:一定要区分函数定义时和函数运行时两个不同的时刻)。特别是在异步编程模型中,函数的定义和运行又分处不同的时间段,那么保持上下文的问题变得更加突出了。因为我们在任务执行一半时把资源交出去没有问题,但当任务需要再次继续时我们必须还原现场。

在这个例子中,db.query作为一个公共的数据库查询方法,把"id"这个业务数据传入给db.query,交由其保存是不太合适的。但我们可以稍作抽象,让db.query再支持一个需要保持状态的数据对象传入,当数据查询完毕后可以把这些状态数据原封不动的回传。如下:

Js代码:

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();

记住这种重要的思路,我们再看看是否还能进一步的抽象?可以的,不过接下的动作之前,我们还要了解在JavaScript中一个函数也是一个对象。一个函数实例fn除了函数体的定义之外,我们仍然可以在这个函数对象实例之本身扩展其他属性,如fn.a=1;受到这个启发我们尝试把需要保持的状态直接绑定到函数实例上:

Js代码

function main(){
    var id = "1";
    var currentState = new Object();
    currentState.person_id =