给Nodejs新手的7条小建议

给Nodejs新手的7条小建议

不知不觉的已经用Node.js有将近一年了,这里我有几条出自实践的node.js建议送给刚刚入门的node.js的朋友。

命名而不是匿名

在JavaScript中,我们可以创建匿名对象和匿名函数。一般来说,匿名函数可以让代码更加简短精悍。

然而,对这些对象或函数进行命名,则有利于调试和优化。以下是我从Chrome DevTool的文章中借用的图片:

Picture

很明显,命名的实体更利于调试和优化。

尽早解引用

V8的GC是清除标记的做法,下面的内容有误导,不推荐阅读了。有关V8 GC的话题,可以阅读我翻译的另一篇文章

JavaScript的GC以一种类似引用计数的算法工作,一个对象当且仅当所有指向它的引用全部释放之后,它本身才会被释放掉。

然而可能你有其他的闭包或者全局对象持有它的引用,从而阻止了垃圾回收。为避免这一现象,应当尽早地解除不必要的引用:

var some_var = new net.Server(); // other code… var i_want_it_temperoray = some_var; some_operation(i_want_it_temperoray); i_want_it_temperoray = null; // derefernce // other code…

如果闭包中尚有一个变量未能被释放,那么整个闭包都有可能无法被回收,从而造成其它对象也无法释放。

现在有很多工具可以辅助我们监视内存的变化情况,比如heapdump、webkit-devtools-agent等等。同时,这也更加说明了为什么要尽可能使用命名而不是匿名的对象。

别复制代码

我用Node.js开发的项目代码变化非常快,重构也很频繁,常常会到处复制代码。然而每当我这样做的时候,后来都会发现某些变量要么就是忘了改名,要么就是有些变量压根就不存在了,导致代码变得极为不稳定。

JS是一门高度动态的语言,它不像C++这样可以利用编译器做静态检查,因此你几乎没有机会依靠工具检查出这些问题。所以我的建议是,尽可能地自己再输入一遍代码,这样IDE或者编辑器有机会利用代码自动补全来为你检查。如果你的IDE还没这功能,那你真得考虑换掉它了。

慎重引入新模块

Node.js社区非常活跃,有成千上万的现成模块可以取用,然而其中有些其实已经没人管了。Node.js的API也常常发生变化,适配node v0.8.x的模块,有可能不支持v0.10.x。

因此当你考虑引入新的模块的时候,务必先去它的pull request列表或者issue列表看看,确认一下这个模块是不是已经被抛弃,或者存在重大的隐患。

用async.js或者promise

Node.js基于回调。因为回调的本质,我们很容易写出嵌套多层的回调函数。回调对于异步来说是好事,但对于代码维护来说却是坏事。

如果你发现代码需要3层以上的回调函数嵌套,那你应该考虑一下,要不要使用async.js或者基于promise概念的模块。

以下是一个用async.js写出来的一系列异步操作:

async.auto([ ‘init_logger’: function(done){ set_handlers_to_logger(done); }, ‘load_config’: [‘init_logger’, function(done){ load_my_config(done); }], ‘init_database’: [‘load_config’, function(done){ connect_to_db_here(done); }], // 假定open_cache与数据库无关 ’open_cache’: [‘load_config’, function(done){ open_cache_here(done); }], // warm_up通常用于从数据库预先抓取一些数据 // 注意,warm_up只会在’init_database’以及’open_cache’结束后执行 ’warm_up’: [‘init_database’, ‘open_cache’, function(done){ fetch_some_data(done); }], ‘init_routers’: [‘load_config’, function(done){ install_routers(done); }], ‘emit_out’: [‘warm_up’, ‘init_routers’, function(done){ notify_others(done); }] ], function(err) { if(err){ // 在这处理异常 } });

我对promise相关的模块不是很熟,但使用Q,应该可以写出这样的代码,,同样易于阅读:

Q.nfcall(function init_logger(){ set_handlers_to_logger(); }) .then(function load_config(){ load_my_config(); }) .then(function init_database(){ connect_to_db_here(); }) .then(function open_cache(){ open_cache_here(); }) .then(function warm_up(){ fetch_some_data(); }) .then(function init_routers(){ install_routers(); }) .then(function emit_out(){ notify_others(); }) .catch(function (error) { // 在这处理异常 }) .done();

选取什么样的库来简化逻辑一般都是随便你。通常来说,async.js非常简单,而Q则更加灵活强大。

比如说async.js中各个函数独立而不嵌套,因此如果你想通过捕获某个函数中的变量就显得有些困难,而在Q中就可以使用嵌套的then语句。

本来我还想写一写不使用任何辅助模块的上述代码,但其实我都很怀疑自己能不能写对。

客户端也许特别慢

用Node.js的时候我们可能会变得有点过于理想化了,因此很容易写出下面这样的聊天室代码:

var net = require(‘net’); var clientList = []; var server = net.createServer(function© { //‘connection’ listener console.log(‘server connected’); clientList.push©; c.on(‘end’, function() { console.log(‘server disconnected’); unpipe_all(c, clientList); remove_from(c, clientList); }); clientList.forEach(function(item){ item.pipe©; // 注意这 c.pipe(item); // 还有这 }); }); server.listen(8124, function() { //‘listening’ listener console.log(‘server bound’); });

我觉得整个结构没什么大问题,但当我们遇上网络状况不好的客户端时,情况就不大好了。这里的两个pipe会把数据缓存在内存中,因此当客户端不能及时接收数据时,这些数据就会大量滞留在内存当中。我们往往假设客户端的速度还不错,但其实那都只是假设!

我想到的一个方法是,用一个中间件来做缓存,当数据太多时使用文件缓冲,而数据不多则用内存,如下:

Delegate delegate; clientList.forEach(function(item){ delegate.append(item); // delegate内部会有一个与文件关联的缓存 // 如果数据太大则把数据存入文件 });

如果你有其它的方案来处理这种情况,不妨也分享一下:)

用事件通知完成,而且不要太早

大多数小对象都是同步构造的,但对于某些封装了复杂操作的对象来说,初始化都有可能是异步的。

如果一个对象需要异步构造,那么最好使用事件通知完成。这时你要留意官方文档 中的一小段话:

This is important in developing APIs where you want to give the user the chance to assign event handlers after an object has been constructed, but before any I/O has occurred.

function MyThing(options) { this.setupOptions(options);

process.nextTick(function() { this.startDoingStuff(); }.bind(this)); }

var thing = new MyThing(); thing.getReadyForStuff();

// thing.startDoingStuff() gets called now, not before.

典型的解决方案是,在构造结束时用process.nextTick来发消息:

function SomeTCPServer(options) { var self = this; // 其他可能异步的初始化工作 process.nextTick(function(){ self.emit(‘ready’); }); }

// 其他代码 var server = new SomeTCPServer(ops); server.on(‘ready’, function when_ready(){ // 其它事情 });

因为用的是process.nextTickwhen_ready不会错过ready事件。

其它建议?

我暂时就想起来这么多,因为我自己也不是Node.js的真正专家。不过我还是希望上面的几条能对新手有所帮助,欢迎大家指出上面的任何错漏,谢啦。


19 回复

给Nodejs新手的7条小建议

不知不觉的已经用Node.js有将近一年了,这里我有几条出自实践的Node.js建议送给刚刚入门的Node.js朋友们。

命名而不是匿名

在JavaScript中,我们可以创建匿名对象和匿名函数。一般来说,匿名函数可以让代码更加简短精悍。

然而,对这些对象或函数进行命名,则有利于调试和优化。以下是我从Chrome DevTool的文章中借用的图片:

命名的实体更利于调试和优化

通过命名函数,调试工具可以更容易地识别出这些函数和对象,从而帮助开发者更快地定位问题。

示例代码:

function namedFunction() {
    console.log("This is a named function");
}

// 使用命名函数
namedFunction();

尽早解引用

虽然V8的垃圾回收机制并不完全依赖于引用计数,但尽早解引用仍然是一个好习惯。这有助于减少内存泄漏的风险。

var some_var = new net.Server();
// other code…
var i_want_it_temporarily = some_var;
some_operation(i_want_it_temporarily);
i_want_it_temporarily = null; // 解引用
// other code…

别复制代码

代码复用是好的,但直接复制粘贴代码可能会导致错误。建议自己手动输入代码,这样可以更好地理解和调试。

示例代码:

// 不要这样做
var server = require('./server');
var client = require('./client');

// 而是这样做
var createServer = require('./createServer');
var createClient = require('./createClient');

慎重引入新模块

Node.js社区非常活跃,但并非所有的模块都值得信赖。在引入新模块前,务必查看其GitHub仓库的状态。

示例代码:

npm install lodash

用async.js或者Promise

Node.js基于回调,容易写出嵌套的回调地狱。推荐使用async.js或基于Promise的模块来简化代码。

示例代码:

const async = require('async');

async.auto({
    init_logger: function(done) {
        set_handlers_to_logger(done);
    },
    load_config: ['init_logger', function(done) {
        load_my_config(done);
    }],
    init_database: ['load_config', function(done) {
        connect_to_db_here(done);
    }],
    open_cache: ['load_config', function(done) {
        open_cache_here(done);
    }],
    warm_up: ['init_database', 'open_cache', function(done) {
        fetch_some_data(done);
    }],
    init_routers: ['load_config', function(done) {
        install_routers(done);
    }],
    emit_out: ['warm_up', 'init_routers', function(done) {
        notify_others(done);
    }]
}, function(err) {
    if (err) {
        // 在这处理异常
    }
});

客户端也许特别慢

不要假设客户端总是响应迅速。处理数据时,应考虑使用文件缓存来减轻内存负担。

示例代码:

let clientList = [];
let delegate = new Delegate();

clientList.forEach(function(client) {
    delegate.append(client);
    // delegate内部会有一个与文件关联的缓存
    // 如果数据太大则把数据存入文件
});

用事件通知完成,而且不要太早

对于需要异步初始化的对象,使用事件通知来完成初始化,并在构造完成后使用process.nextTick来触发事件。

示例代码:

function SomeTCPServer(options) {
    var self = this;
    // 其他可能异步的初始化工作
    process.nextTick(function() {
        self.emit('ready');
    });
}

// 其他代码
var server = new SomeTCPServer(ops);
server.on('ready', function when_ready() {
    // 其它事情
});

希望这些建议能帮助新手更好地理解并使用Node.js。欢迎提出更多建议和改进意见。


这个要 mark 的~其实感觉自己总是被异步绕得很晕,抓狂过很多次,不过写多了自然就熟悉啦~

赞楼主

确实不错,支持

我就记得,有一次读一个很大的文件,直接把我内存读爆了。今天看到楼主解引用这段,我觉得有必要看看我的代码是不是没有解引用。

读大文件尽量用流的方式处理,虽然现在node的流还不是特别完善:)

赞,挺好~~

好顶赞 写的稍微有点简单。 不过确实给我指了些学习方向

nice, mark一下

必须mark~

赞!非常有帮助。刚初学一个月,摸索中 ……

gc不是改标记清除了么? 自豪地采用 CNodeJS ionic

楼主,除非像是函数,或者变量内部有对闭包变量的引用才会导致闭包变量不会被释放。 普通的变量是不会的,

 var globals = [];
 function foo() {
   var a = 1;
   return function () {
     var b = a + 1;
 	return b;
   }
 }
 globals.push(foo()); //  may cause variable a unable to be GCed.
 globals.push(foo()()); //  may not!

尽早解引用的那部份代码,如果是在一个 function 体内,不设置 = null ,也是安全的。 而示例代码中没有新的 function ,也就是它没有产生“闭包”。 如果 some_operation 里面有闭包,也不会导致当前 i_want_it_temperoray 被引用。 且 some_operation 里面的闭包引用的是属于它的 i_want_it_temperoray 变量,设置当前 i_want_it_temperoray = null 是无法解除 some_operation 内部产生的闭包的引用的。 结论是设置 null 是解决不了任何问题的。也不会有明显的效果。

代码改一下吧,不太方便看

好久以前的文章了,怎么被挖坟了OTZ

关于内存泄露,有个有意思的补充给大家:http://info.meteor.com/blog/an-interesting-kind-of-javascript-memory-leak

另外引用计数的确过时啦,现在确实是标记清除了,至少V8现在是

给Node.js新手的7条小建议

命名而不是匿名

在JavaScript中,我们可以创建匿名对象和匿名函数。虽然匿名函数可以使代码更加简洁,但在调试和优化过程中,对对象或函数进行命名可以提供更多的信息。例如:

const myFunction = function namedFunction() {
    console.log('Hello World');
};

命名的函数有助于调试工具识别函数并提供更好的堆栈跟踪信息。

尽早解引用

尽管最初的描述关于V8的垃圾回收机制有误,但确实需要注意释放不再使用的引用以防止内存泄漏。例如:

let someVar = new net.Server();
// 其他代码...
let temporaryReference = someVar;
someOperation(temporaryReference);
temporaryReference = null; // 解引用

别复制代码

复制代码会导致错误,建议重新输入代码,并利用IDE的自动补全功能来减少错误。

慎重引入新模块

在引入新模块前,确保其维护状态良好,且兼容当前的Node.js版本。可以通过查看模块的Pull Request和Issue列表来评估。

用async.js或者Promise

对于复杂的异步操作,推荐使用async.jsPromise来简化代码。例如:

async.js 示例

const async = require('async');

async.auto({
    initLogger: function(done) {
        setHandlersToLogger(done);
    },
    loadConfig: ['initLogger', function(done) {
        loadMyConfig(done);
    }],
    initDatabase: ['loadConfig', function(done) {
        connectToDbHere(done);
    }]
}, function(err) {
    if (err) {
        // 处理错误
    }
});

Promise 示例

const Q = require('q');

Q.nfcall(initLogger)
    .then(loadConfig)
    .then(initDatabase)
    .catch(error => {
        // 处理错误
    })
    .done();

客户端也许特别慢

在处理网络通信时,避免直接将数据流相互传递,而应使用中间件来管理数据缓存。例如:

class Delegate {
    append(client) {
        // 管理缓存
    }
}

let clientList = [];
let delegate = new Delegate();

clientList.forEach(client => {
    delegate.append(client);
});

用事件通知完成,而且不要太早

如果对象需要异步初始化,使用process.nextTick来触发事件。例如:

function MyThing(options) {
    this.setupOptions(options);

    process.nextTick(() => {
        this.startDoingStuff();
    });
}

const thing = new MyThing();
thing.on('ready', () => {
    // 处理其他事情
});

这些小建议可以帮助Node.js新手更好地理解和使用这门技术。希望对大家有所帮助!

回到顶部