深入理解node.js异步编程(闭包,事件,内存回收,eventloop,io)

最近没啥状态,没怎么看代码,要看的书也没怎么看。。。希望状态能触底反弹吧。。。

这是自己很早很早之前就打算写的一篇博客,最近发现自己将很多javascript的概念淡忘了,这可不好,那就借此机会逼自己写这篇博客,顺便也能复习一下javascript的知识。、。。。


其实个人觉得javascript语言本身是非常的简单的,而且其程序的运行层次以及结构也都还算比较的简单,原型链以及作用域链算是其最大的特点了吧,再加上有了node.js与之结合,将javascript的特点发挥的淋漓尽致,其回调的编程方式非常适合异步IO编程,加上其单线程的特性,轻轻松松的就实现了在高性能服务器编程中常提到的Reactor模式,这种模式要让java去实现的话,嗯,挺麻烦的,当然这里并不是黑java,只是说它要实现的难度比较大,而且对程序员的水平要求比较高,毕竟仅仅java的并发编程就可以pass掉一大批的java程序员了。。。。


好了,闲话先不说了,来进入文章的主题吧,要说回调编程,就不得不先说一个概念:闭包

先说说闭包的概念吧,这里直接引用《javascript语言精粹》中的定义:函数可以访问他被创建时所处的上下文环境,这被称作闭包。(javascript的作用域链是实现javascript闭包实现的关键)

这里拿一段代码来说明闭包吧:

var out = 123;

function outter(){
	var aa = 1111;
	return function() {
		var bb = 31231;
		console.log(out);
		console.log(aa);
		console.log(bb);
	}
}

var fn = outter();
fn();

这里就会涉及到三个作用域:

(1)全局作用域,里面的变量有out,outter

(2)outter函数的作用域,它自己内部的有变量aa,另外因为它在全局作用域中创建,所以它的作用域将会链接全局作用域,因此它的变量就有out,outter,aa

(3)outter函数创建的匿名函数的作用域,它自己内部申明的变量bb,然后因为其在outter函数内部创建,所以它将会链接outter函数的作用域链,而outter函数又链接了全局作用域,所以它的变量有out,outter,aa,bb

这里用一张图来说明一下吧:

深入理解node.js异步编程(闭包,事件,内存回收,eventloop,io)_第1张图片

上面的图形基本将作用域的问题说明的比较清楚了吧,比较简陋,还有很多东西没有刻画出来,例如this变量,arguments变量啥的,这里就不细说了,关于闭包的实现以及作用域链的东西可以参见我以前写过的博客。。

上面的代码简单的说明了啥是闭包,由于这里作用域的相互关联,那么这里变量的销毁,管理就是一个比较值得注意的地方了,毕竟javascript也是自己来gc的。。。但是很多时候如果滥用闭包将会造成严重的内存泄露问题。。所以要用好javascript最好还是要注意一下,起码要知道自己创建的变量将会在什么时候被销毁吧。。。

这里还是拿上面的代码来说明,outter函数执行完之后,我们将会得到创建的匿名函数的引用,并用fn来指向它

学过计算机专业基础知识的人都知道,一个程序说白了就是一个执行栈(或者说上下文环境的栈,context),首先有一个最开始的执行环境,当有函数方法啥的要执行之后,就会在栈中压入一个新的执行环境,当函数方法退出之后,刚刚压入的执行环境就退出,知道整个栈都空了,那么整个程序就算结束了。。。(当然这说的是单线程程序)

一般情况下对于一个变量,如果没有别的地方引用它了,那么随着其所处的执行环境退出,它将会被销毁。。。这里来看看在outter函数内部创建的变量aa,当outter函数执行完之后,outter函数自己的执行环境将会被销毁,那么aa变量所处的执行环境就不存在了,如果按照常规的想法,它将会被销毁,但是由于我们返回的匿名函数的作用域中有对aa变量的引用,那么aa变量的内存的生命周期就被返回的匿名函数延长了,只有匿名函数被销毁了,aa变量的引用数才回变成0,它才回最终被销毁。。。。

所以在进行回调编程中一定要慎重的考虑闭包中变量的问题,一定要保证变量最终会被正确的销毁。。。

这里说了这么多,用另外一段稍微复杂一点的代码来举例子:

var fs = require("fs");

function readFile(path, cb) {
	var stream = fs.createReadStream(path);
	var out = new Buffer(0);
	stream.on("data", function(data){
		console.log(111);
		out = Buffer.concat([out, data], out.length + data.length);
	});
	stream.on("end", function(){
		cb(out);
	});
}

readFile("aa.txt", function(data){
	console.log(data.toString("utf8"));
});

这是一段node.js的代码,好吧,这篇文章的主要目的就是要将这段代码搞透彻。。。。什么叫搞透彻呢,嗯,就是跟踪这段程序的开始到结束的每一步的执行。。。

好了,那就一步一步的来吧:

首先是开始执行,先来看看最开始的时候程序的栈吧:

深入理解node.js异步编程(闭包,事件,内存回收,eventloop,io)_第2张图片


当前栈里面就只有一个执行环境,它的作用域中有fs以及readFile两个变量,然后开始执行如下的代码

readFile("aa.txt", function(data){
	console.log(data.toString("utf8"));
});

他将会创建一个新的执行环境,然后将其入栈,那么图形就变成了这个样子:



这里也就是开始了readFile函数的执行,我们知道在readFile中IO方法的执行都是异步执行的(嗯,这个是node.js的内容了),那么整个readFile函数执行完了之后,其执行环境将会出栈,那么又变成了这个样子,


但是这里要注意的是,对于stream变量,out变量,以及传进去的回调函数cb变量,他们并不会被销毁,因为在函数内部创建了两个匿名函数都将会有他们的引用,所以这几个变量的生命周期被这两个匿名函数延长了。。。

然后这里又引入了另外一个问题,这里并没有哪里直接引用了这两个匿名函数,那么这两个匿名函数就应该直接被销毁啊,嗯,其实是stream对象引用了这两个匿名函数,

那么这里又出问题了,那么谁又来引用stream对象呢,如果仅仅是两个匿名对象来引用这个stream对象,那么它应该直接被销毁,因为当前栈都退出了,没有任何地方可以访问他们了。。。好吧。。。那么程序要正确的运行,必然有地方会引用这个stream对象,嗯,这个待会说。。。

那么readFile函数执行完了之后,外面的整个程序貌似也就执行完了,那么执行栈中最开始的都应该要出栈,那么就变成了

深入理解node.js异步编程(闭包,事件,内存回收,eventloop,io)_第3张图片

这里整个执行栈都空了,那么程序应该要退出了啊,所有的变量都应该要销毁啊。。那程序岂不是就错误啦,但是程序其实正确运行的啊,文件中的内容被正常输出了。。。

好吧,要弄明白这个东西,那么也就必须要解决上面提出来的stream对象的引用问题,这里就需要提出一个概念,事件循环(eventloop):

上面的图形确实没错,整个程序看起来的确已经运行完了,但是这仅仅是整个javascript程序运行的第一步,接下来整个程序将会进入事件循环阶段。。。。

这里stream对象也是被这个叫eventloop的东西引用的。。。。因为stream对象依然被引用,所以上面程序代码中创建的变量都不会被销毁,因为整个程序的执行还可以通过stream对象来回溯找到这些变量的引用,只有stream被销毁了这些变量才回相应的被销毁。。。

所以就必须要搞清楚evenloop到底干了啥。。。如果这里对nginx或者netty的运行原理有一定了解就很容易理解了,其实接下来eventloop要做的事情,就是不断的执行对当前注册的所有的IO对象的IO事件,以及定时时间(setTimeout),用一张图来说明:

深入理解node.js异步编程(闭包,事件,内存回收,eventloop,io)_第4张图片

整个eventloop要做的事情就是执行所有注册的IO对象的IO事件,以及相应的事件回调,还有就是定时事件。。。

这里就知道上面stream其实是被注册到了EventLoop上面了,而那些匿名函数其实就是它的事件回调方法。。。

好吧,到这里应该知道为啥程序依然能正确输出了吧。。。

好了,那么程序将会在什么时候退出呢,这些变量将会在啥时候被销毁呢。。?

这里可以看到,stream对象的end事件,它就是当输入流已经读完之后的事件,然后执行end的回调方法,也就是后面的匿名函数。。。

之后呢,stream对象将会从eventloop上被取消注册,那么到此为止,所有的执行环境都没有stream对象的引用了,从而上面创建的所有变量都会随之被销毁。。

然后eventloop将会发现没有任何注册的对象了,从而整个eventloop也就相应的退出了。。。

到此为止,整个node.js程序也就退出了。。


好吧,写的够乱的。。。。不过复习的作用也算是起到了吧。。。。。

另外node.js底层eventloop用的是libuv,其实跟libev,nginx甚至netty的实现应该没有啥本质的区别。。。

最后想说的是,其实不管是啥,程序都一样,就是一个栈、、、、、



你可能感兴趣的:(深入理解node.js异步编程(闭包,事件,内存回收,eventloop,io))