之前也说了,开始玩儿nodejs,然后拿cheater下手。做得差不多了,总结一下。会有几篇单独的文章,省得看起来乱。
nodejs的基本思想就是异步,这对传统编程模型是彻底的颠覆。刚开始很不习惯,死得很惨,程序怎么都不按我想的跑。一个简单的例子就是http的request,调用不会等待response而是立即返回,然后通过回调函数处理response。
对于这种异步的模型,通常有两种办法,一是回调函数+事件传递,二是直接用回调函数处理所有事情。简单说明一下,第一种在回调函数中不做什么事情,只是简单地发出一个事件,由事件的listener处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var cheater = function () { this.on('login', function (retryTimes) { this.once('login_ok', function () { // note that "once" is used, not "on" this.emit('heartbeat', , ); }); this.once('login_nok', function () { var that = this; if (retryTimes < CheaterConfig.loginMaxRetryTimes) { timeoutId = setTimeout(function () { that.emit('login', retryTimes + 1); }, CheaterConfig.loginRetryInterval * 1000); } }); client.login(); // send out http request, later login_ok or login_nok event will be emitted }); } |
这里没有给client.login()注册任何回调函数,client.login()自己在调用https.request时注册了一个回调函数,这个函数仅仅用来发事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | CheaterClient.prototype.login = function () { var fs = require('fs'); var options = { cert: fs.readFileSync(CheaterConfig.cert), host: CheaterConfig.loginHost, port: CheaterConfig.loginPort, path: CheaterConfig.loginUrl, method: 'POST' }; var request = https.request(options, function (response) { response.setEncoding('ascii'); response.on('data', function (data) { if (data is valid) { this.cheater.emit('login_ok'); // everything is fine } else { this.cheater.emit('login_nok'); // oops, bad things just happened }); }); request.write(CheaterConfig.loginInfo()); request.end(); }; |
值得注意的是在注册login_ok和login_nok的时候并不是使用“on”,而是用了“once”,原因就在于为了不把那个retryTimes传来传去,这里用了closure,如果用“on”的话,每次login事件被捕获的时候都会注册login_ok和login_nok的listener,然后就……杯具了。
对于第二种方案就不多说了,比较容易理解,但是个人感觉代码会比较混乱,所以我没有使用。
还有一点值得一提,也是折磨我许久。看下面这段代码:
1 2 3 4 5 6 7 8 | Cheater.prototype.heartbeat = function (livingTime, failedTimes) { if (livingTime < 3600 / CheaterConfig.heartbeatInterval * CheaterConfig.hoursToLive) { setTimeout(function () { this.heartbeat(livingTime + 1, ); this.doSomething(); }, CheaterConfig.heartbeatInterval * 1000); } }; |
先梳理一下这段代码都干了什么。login之后开始做heartbeat,隔一段时间一次,然后过了几个小时之后logout。看起来很美好,heartbeat递归调用,直到某次调用返回之后doSomething()。
但是,杯具就发生了,doSomething被提前调用了!我们来看看为什么。
下面是函数调用桟(比较土,凑合着看吧):
cheater.heartbeat(0, 0); --> setTimeout(function, interval); --> heartbeat(1, 0); --> setTimeout(function, interval); // 这里异步出现了,线程在这一点上留下一个continuation,然后这次递归调用结束了! --> doSomething(); // 并不像我们所想的,doSomething()被提前调用了,在逻辑上的递归还没有结束之前!
这个问题害得我调了很久,看来惯性思维确实很难改变。