一 异步与阻塞,事件驱动与顺序执行
1.什么是异步,什么是事件驱动,异步有什么好处,有什么坏处
A君和B君今天都是计划去银行办点事,然后去超市买点日用品.他们都来到银行,A去自动提款机那里开始排队,前面大概有20来人,她只能依次排队等,取到钱后她再去超市.B去到排队机抽了个号码,他一看前面还有很多人,预计要比较长事件,然后去隔壁超市找要买的东西,听到银行广播自己号码时候,再回来银行办事.
对于A君,我们叫同步,程序只能按预定的顺序执行,遇到耗时长的操作时候,需要等待其完成才能执行下一步任务.对于A君,他有个好处就是按部就班,不怕出现意外情况,例如身上钱不够这些问题.但明显会消耗更多时间.
对于B君,我们叫异步,程序遇到需要等待长的操作时候,不等待其完成而马上执行下一步任务,再等到长时间操作完成后再回头去继续处理后续问题.这样好处是节约时间,但如果你事情多的话,这样就变得有些乱,不好管理.
而银行的广播会触发B君回去银行,这个我们叫事件驱动.当然这样不好的地方很多,例如出错处理会更加麻烦,事情会变得混乱不好整理.
2 如何阅读node的代码
由于事件驱动的存在,我们不能像其他的一些代码那样顺序查看,我们应该先大概预览一次代码,然后以"事件"为关键词,搜索代码,找到代码的开头入口.
例如一个简单web程序:
var http = require(‘http‘);
var url = require(‘url‘);
var Web = function(req,res){
this.req = req;
this.res = res;
};
Web.prototype[‘/hello‘] = function(){
this.res.end("hello world");
}
Web.prototype[‘/404‘] = function(){
this.res.
this.res.end("404 not found");
}
http.createServer(function(req, res){
var reqinfo = url.parse(req.url);
var web = new Web(req, res);
var path = reqinfo.pathname.toLowerCase();
if (path && web[path]){
try{
web[path]();
}catch(err){
throw err;
}
}else{
web[‘/404‘];
}
}).listen(80);
你可以发现,如果你从头开始看,它是不会符合事情的顺序来的,我们应该从http.createServer这里开始入手.而段代码却是在程序的最后面.
所以看node的代码,你要做的第一件事不仔细看每个代码,而是快速浏览一次,并且找到正确的入口.
(PS:最开始,我作为PHP程序员进入我的前公司,但措手不及的0基础接手了一个离职的研究所写node半成品(半成品都没,因为程序运行不了)项目,我用了PHP的习惯来查看代码,寻找代码的思路,导致了我一周完全摸不着头脑.这种痛苦至今刻骨铭心,也感谢这种刻骨铭心,让我学其他各种语言更加得心应手)
二 回调与避免深度嵌套
1.深度嵌套的典型例子
如下面的这个读取文件内容的函数:
fs.readFile(‘/etc/passwd‘, function (err, data) {
if (err) throw err;
console.log(data);
});
那,我们读取两个文件,将这两个文件的内容合并到一起处理怎么办呢?大多数接触js不久的人可能会这么干:
fs.readFile(‘/etc/passwd‘, function (err, data) {
if (err) throw err;
fs.readFile(‘/etc/passwd2‘, function (err, data2) {
if (err) throw err;
// 在这里处理data和data2的数据
});
});
那要是处理多个类似的场景,岂不是回调函数一层层的嵌套?
我们常常把这个问题叫做”回调黑洞”或”回调金字塔”:
doAsync1(function () { doAsync2(function () { doAsync3(function () { doAsync4(function () { // .... doAsyncN(..) }) }) })
这种层层嵌套的代码给开发带来了很多问题,主要体现在:
1.代码可读性变差(非常严重)
2.调试困难
3.出现异常后难以排查
4.改写困难.
回调黑洞是一种主观的叫法,就像嵌套太多的代码,有时候也没什么问题。为了控制调用顺序,异步代码变得非常复杂,这就是黑洞。有个问题非常合适衡量黑洞到底有多深:如果doAsync2发生在doAsync1之前,你要忍受多少重构的痛苦?目标不单单是减少嵌套层数,而是要编写模块化(可测试)的代码,便于理解和修改。
2.node v0.1.1版本后的解决方法
2.1 Generator
Generator是为JavaScript设计的一种轻量级的协程。它通过yield关键字,可以控制一个函数暂停或者继续执行, 本身generator的引入并不是用来解决异步问题的,但yield和next接口恰巧可以为我们解决深层回调的问题,让我们接受异步产生的结果,继续完成接下来的任务,与回调的目的一致。
generator实际上是一种迭代器。它很像一个可以返回数组的函数,有参数,可以调用,并且会生成一系列的值。然而generator不是把数组中的值都准备好然后一次性返回,而是一次yield一个,所以它所需的资源更少,并且调用者可以马上开始处理开头的几个值。简言之,generator看起来像函数,但行为表现像迭代器。这感觉代码中使用function*来表示generator很有相似.
2.2generator使用, yied,next,function*和co库.
(如果你熟悉go语言或者python 你肯定对这个东西不陌生.)
Node对generator的支持是从v0.11.2开始的,但因为还没正式发布,所以需要指明--harmony或--harmony-generator参数启用它(NodeJS使用V8引擎,而V8引擎对ES6中的东西有部分支持,所以在NodeJS中可以使用一些ES6中的东西。但是由于很多东西只是草案而已,也许正式版会删除,所以还没有直接引入。而是把他们放在了和谐(harmony)模式下,如果你需要用,需要指明)。
例如运行文件test.js 我们需要
#node --harmony test.js
#forever start -c "node --harmony" test.js //如果使用forever启动,这也是forever支持ES6的方法
首先定义一个简单的generator,并使用它:
function* Foo(x) { yield x + 1; var y = yield null; console.log(y); return x + y; } var foo = Foo(5); foo.next(); // { value: 6, done: false } foo.next(); // { value: null, done: false } foo.next(8); // { value: 13, done: true }
generator的定义跟普通函数差不多,只是在 function 关键字后面多了一个 *号。而调用generator后会返回一个generator对象,其中保存了generator的内部执行状态。每调用一次generator的next方法,就会得到一个包含执行结果的对象,含有两个域 value 和 done 。 value 是此次执行generator的返回值, done 为generator是否已经执行完的标志。如果对 done 为 true 的generator对象调用next方法则会抛出Error: Generator has already finished 错误。
generator中使用了一个新的关键字 yield ,它的作用与 return 差不多,除了可以从generator中返回值给外部使用外,还可以暂停该generator的执行,也可以通过next传递值给generator的上一次中断位置(因此y得到8)并从上次中断位置继续执行代码。
我们关注next的代码
var foo = Foo(5); //创建对象,代码不执行 foo.next(); // { value: 6, done: false } next传入了undefined,执行了 x+1 返回 x+1的结果 foo.next(); // { value: null, done: false } next 传入了undefined,执行了 null,返回 null的结果 foo.next(8); // { value: 13, done: true }传入了8 并被y接收 执行了console.log(y)和return x+ y;
待续