node.js 之异步篇

来源:互联网 发布:center os 安装软件 编辑:程序博客网 时间:2024/06/16 18:51

提到javascript的异步,setTimeout就浮现在眼前。

函数原型:setTimeout(function, millisecond,[arg1],[arg2]...)

var log = { i : function(info) {console.log(info);}};

setTimeout(log.i, 1000, "do...");
log.i("set timeout over");

输出log:

set timeout over
do...

在延时1000毫秒后,执行了设置的log输出,而且setTimeout时并没有阻塞后一行程序的执行。

有点异步的感觉,不过到底是不是真的异步,下面再来测试一下:

setTimeout(log.i, 1, "do...");
log.i("start:" + new Date().getMilliseconds());
for(var n = 0; n < 99999999; n++) {
    var n1 = 0;
    n1++;
}
log.i("over:" + new Date().getMilliseconds());

输出log:

start:156
over:485
do...

设置延时时间1毫秒,中间的fo就循环磨耗掉了300多毫秒,按照一般的认识,应该在for循环期间就会执行setTimeout中的函数,实际结果是在执行完所有代码后在执行。

那么计算时间是一哪一个点为基准的,setTimeout?还是程序执行完的时候?

改造一下代码,再来试一下

var log = {
    i : function(info) {
        var d = new Date();
        var tstr = d.getSeconds() + ":" + d.getMilliseconds();
        console.log("[" + tstr + "]:" + info);}
};

setTimeout(log.i, 500, "do...");
log.i("start:" + new Date().getMilliseconds());
for(var n = 0; n < 99999999; n++) {
    var n1 = 0;
    n1++;
}
log.i("over:" + new Date().getMilliseconds());

log输出:

[36:796]:start:796
[37:109]:over:109
[37:296]:do...

由此可见计数还是以setTimeout时为基准的,如果把中间的循环次数加大一些,让耗时大于timeout的时间,此时的log输出为:

[25:405]:start:405
[26:827]:over:827
[26:827]:do...

嗯,程序执行完后马上就执行setTimeout。看来只是在空闲的时候才会去处理setTimeout。


那么setTimeout中的函数如果耗时很多,会是什么情况:

var log = {
    i : function(info) {
        var d = new Date();
        var tstr = d.getSeconds() + ":" + d.getMilliseconds();
        console.log("[" + tstr + "]:" + info);},
    j : function(m) {
        log.i("[" + m + "]consuming start");
        for(var n = 0; n < 99999999; n++) {
            var n1 = 0;
            n1++;
        }
        log.i("[" + m + "]consuming end");
    }
};

for(var m = 0; m < 2; m++) {
    setTimeout(log.j, 100, m);
}
    
log.i("start:" + new Date().getMilliseconds());
for(var n = 0; n < 999999990; n++) {
    var n1 = 0;
    n1++;
}
log.i("over:" + new Date().getMilliseconds());

log输出:

[47:781]:start:781
[49:187]:over:187
[49:187]:[0]consuming start
[49:297]:[0]consuming end
[49:297]:[1]consuming start
[49:406]:[1]consuming end

在log中加了一个耗时的函数j,从输入的结果上来看,setTimeout中的函数也是按照顺序,等待上一个结束后才继续下一个函数的执行。

从程序的角度来看,程序先创建一个时间队列,调用setTimeout时将函数添加到队列中去,同时把该时点的时间也放进去。当然也可想象将当前执行代码的函数也放到队列中,然后程序再挨个检查执行时间是否符合,如果设定时间小于或等于当前时间就执行,同时从队列中删除,不满足的等待下一次的检查。搞清了这一点上面的程序就很容易理解了。


这不是多线程的风格啊。node.js用单线程来实现多线程的事,必然会将众多的事件扔到时间队列中,然后挨个执行,这就要求队列中的每个函数不能消耗太多的时间,不然会阻塞后面函数的执行。对于单个cpu来说,在其基础上实现多线程,无非是把众多需要同时执行的代码分成代码片断,一个程序执行一点后再切换到另一个程序上去执行一段,速度快了于是就有了同时执行的感觉,如果放慢足够的时间,程序片段足够大,多线程的表现跟上面的例子是一样的。

如果要争论多线程和单线程来实现web开发哪个好,仁者见仁智者见智,对于程序员来说客户需求的就是最好的。

看过一个记录片,一个高僧喜欢品各种各样的好茶,品到最后却发现,一杯淡淡的茶就足够了。程序语言何尝也不是这样的呢,写到最后简单的javascript就足够了,呵呵,开个玩笑,并不是特别推崇javascript。其实一个人程序写多了,对程序语言反而淡了。


话归正题,单线程实现,注定了node.js开发时需要考虑更多、更基本的东西,当然node.js不是使用setTimeout来构建其核心框架的,后面的系列文章中会提到。

再来看看setTiemout中的参数millisecond,如果设置成0、负数或者undefined,结果会怎么样呢:

var show = {
    log : function(info) {console.log(info);}
};

var timeout = function(){
    return 0;
};

setTimeout(show.log, 2, "*******************");
setTimeout(show.log, 1, "-------------------");

for(var i = 0; i < 5; i++) {
    setTimeout(show.log, timeout(), "delay 1000:" + i);
}
show.log("after settimeout");
log输出:

after settimeout
-------------------
delay 1000:0
delay 1000:1
delay 1000:2
delay 1000:3
delay 1000:4
*******************

从结果来看,0相当于1,将timeout的返回值改成其它的值,负数或undefined都相当于设置为1.

再来看一个有趣的现象:

setTimeout(show.log, 2, "*******************");
setTimeout(show.log, 1, "-------------------");
for(var i = 0; i < 5; i++) {
    setTimeout(function(){console.log(i);}, timeout());
}

log输出:

-------------------
5
5
5
5
5
*******************

按照常理,log中应该输出0到4。为什么眼睁睁地看到代码缓缓流过,却不会执行,等黄花都谢了才是真正执行的时候。 这就是异步的特点,i对于setTimeout中的function来说相当于一个全局变量,只是申明会用到这么一个变量,for循环结束时,i变量的值已然等于5。等循环结束后程序去检查setTimeout列表,执行其中的函数,由于变量引用的原因,打印出来的值就是5。再来验证一下:

setTimeout(show.log, 2, "*******************");
setTimeout(show.log, 1, "-------------------");
for(var i = 0; i < 5; i++) {
    setTimeout(function(){console.log(i);i=10;}, 100);
}
log输出:

-------------------
5
10
10
10
10
*******************

在function中将i设置为10,一个未来的赋值不会影响for循环的条件,但会影响其它引用i变量的程序。


再深入一步:

var i = 123;
setTimeout(function(){console.log(i)}, 100)
var i = "abc";

log输出:

abc

看来,function在执行的时候,本地作用域中找不到i,于是返回上一层,找到了最后一个i:“abc”


如果利用闭包,可以输出123,稍微改一下程序:

var i = "abc";
var a = function(){
var i = 123;
setTimeout(function(){console.log(i)}, 100);
}();
var i = "abc";
log输出:
123


把function中的变量引用改成参数形式就可以避免变量带来的问题:

setTimeout(show.log, 2, "*******************");
setTimeout(show.log, 1, "-------------------");
for(var i = 0; i < 5; i++) {
    setTimeout(function(n){console.log(n);n=10;}, timeout(), i);
}

log输出:

-------------------
0
1
2
3
4
*******************
设置成参数,setTimeout时拷贝了i的一个副本,这只是针对于数据基本类型来说的,

var obj = {p:1};
setTimeout(show.log, 2, "*******************");
setTimeout(show.log, 1, "-------------------");
for(var i = 0; i < 5; i++) {
    setTimeout(function(n){console.log(obj.p);obj.p = 101;}, timeout(), obj);
}
log输出:

-------------------
1
101
101
101
101
*******************

此次不是变量的副本,而是指针了,一个地方更改,其它地方都会受到影响。常用的数组也会发生变化的,需要注意。



最后提一下clearTimeout,这个函数太简单了,没有多少玩法。

setTimeout后,如果某种原因又不想让其function执行,clearTimeout可以在其执行前取消。setTimeout将function放入队列中,而clearTimeout则是将function从队列中删除。

var log = {i : function(info){console.log(info);}};
var handle = setTimeout(log.i, 1000, "begin");
clearTimeout(handle);

上面的例子中,永远也不会打印log了,即使在clearTimeout语句前面插入很耗时的程序。

clearTimeout唯一的参数就是setTimeout时返回的对象。

再看看一个比较有意思的例子:

var log = {i : function(info){console.log(info);}};
var timeoutobj = setTimeout(
    function(){
        log.i("begin");
        clearTimeout(timeoutobj);
        log.i("end");
    }, 1000
);

log输出:

begin
end

即使在timeout的函数中取消timeout,但是还会输出end。

为啥,不为啥,到达设置的时间时,首先会将function从队列中删除,然后再执行function中的代码。通过在function中调用clearTimeout,去队列中删除function已然没有任何意义了。这个回答很不专业,只是为了便于理解。来个稍微专业一点的:

var log = {i : function(info){console.log(info);}};
var timeoutobj = setTimeout(function(){}, 1000);
var clearobj = setTimeout(function(){}, 1000);
clearTimeout(clearobj);
log.i(timeoutobj);
log.i("----------clear timeout----------------");
log.i(clearobj);

log输出:

{ _idleTimeout: 1000,
  _idlePrev:
   { _idleNext: [Circular],
     _idlePrev: [Circular],
     msecs: 1000,
     ontimeout: [Function: listOnTimeout] },
  _idleNext:
   { _idleNext: [Circular],
     _idlePrev: [Circular],
     msecs: 1000,
     ontimeout: [Function: listOnTimeout] },
  _idleStart: 1393490927999,
  _onTimeout: [Function],
  _repeat: false }
----------clear timeout----------------
{ _idleTimeout: -1,
  _idlePrev: null,
  _idleNext: null,
  _idleStart: 1393490927999,
  _onTimeout: null,
  _repeat: false,
  ontimeout: null }
setTimeout返回对象中的属性不想去太多地了解,但从log可以看出,队列是一个双向队列,setTimeout函数往这个队列中插入了一个节点。与之对应,clearTimeout函数把节点从队列中摘取出来,并将节点上的信息全部初期化。


---------------------------------------------------------------------------

还有一个函数setInterval,跟setTimeout完全类似,setTimeout只执行一次,而setInterval是循环执行,相当于

setInterval = setTimeout(function(){setTimeout})

其函数原型:setTimeout(function, millisecond,[arg1],[arg2]...)

相对应也有一个clearInterval

先来看看例子:

var log = {i : function(info) {console.log(info);}};
var callback = function(handle){
    log.i("call back");
    clearInterval(handle);
    log.i("clear interval");
};
var interobj = setInterval(
    function(func){
        log.i("setInterval");
        func(interobj);
    }, 500, callback);

log输出:

setInterval
call back
clear interval

写这个例子的时候犯了一个错,导致clearInterval无效:

var interobj = setInterval(
    function(func, handle){
        log.i("setInterval");
        func(handle);
    }, 500, callback, interobj);

原本想将setInterval返回的句柄也通过参数传递个回调函数callback,在回调函数中执行clearInterval,结果log根本就停不下来。

分析后才发现,在设置参数interobj时,setInterval还没有执行,也就是说interobj等于undefined,clearInterval(undefined)当然没有任何效果。

 一般情况下,setInterval可以当成定时器来使用,是不是每隔设定毫秒数就会执行一次?

var log =
{i : function(info) {
        var d = new Date();
        console.log("[%d %d:%d]%s", d.getMinutes(), d.getSeconds(), d.getMilliseconds(), info);},
    j : function(milliseconds) {
        var d = new Date();
        for(;new Date().getTime() - d.getTime() < milliseconds;);
    }
};

function createFunc(){
    var counter = 0;
    return function() {
        log.i("interval>" + counter++);
        log.j(300);
        if (counter == 5)clearInterval(handle);
    }
}
var handle = setInterval(createFunc(), 100);

在log中新加了一个休眠函数j,用来磨洋工,定时不是很准,凑合用吧。

log输出为:

[51 41:250]interval>0
[51 41:875]interval>1
[51 42:297]interval>2
[51 42:719]interval>3
[51 43:141]interval>4

log间的时间差在400左右,刚好是setInterval中的时间+休眠函数时间,由此可见,setInterval不会按照函数中设置的时间定时执行,而是本次执行结束后,延时指定时间数后再次执行,也说明了上面的猜测是对的:

setInterval = setTimeout(function(){setTimeout。。。。。})

setTimeout执行后其节点会从事件列表中删除,而setInterval不会被删除,永远有执行的机会。

---------------------------------------------------------------------------

如果不想延时怎么办?

一个方法就是setTimeout(function, 0),设置延时时间为0

还有一个方法就是立即调用函数,从异步的角度来说,这个方法太不靠谱了,调用的函数可能很耗时间,如果直接调用会影响后续程序的执行,setTimeout的目的就是当前并不想执行函数,而是在当前程序执行结束后再去调用。

setImmediate(callback,[arg1],[arg2]....)

这个函数就是让callback在当前程序执行结束后马上执行

setImmediate(log.i, "immediate");
log.i("end");

log输出:

[11 15:499]end
[11 15:687]immediate


相应的还有一个clearImmediate

var handle = setImmediate(log.i, "immediate");
clearImmediate(handle);
log.i("end");

log输出:

[13 0:46]end

单纯从表面上看,没啥新鲜感觉。那么来猜猜在内部setImmediate和setTimeout(0)是不是一回事。

setTimeout(log.i, 0, "setTimeout1");
setImmediate(log.i, "immediate1");
setImmediate(log.i, "immediate2");
setImmediate(log.i, "immediate3");
setTimeout(log.i, 100, "setTimeout2");
log.j(200);
log.i("end");

log输出:

[16 16:132]end
[16 16:327]setTimeout1
[16 16:327]setTimeout2
[16 16:327]immediate1
[16 16:327]immediate2
[16 16:327]immediate3

在程序末尾延时200毫秒,对于所有的setTimeout来说都可以执行了,setImmediate更不用说了:马上执行。从结果来看setTimeout的优先都比Immediate要高,这马也太慢了。在网上搜索了一下:

①setTimeout, setInterval使用了一个队列

②setImmediate使用了另外一个队列

③另外还有一个IO使用的队列

优先顺序是① > ③ > ②

这个马上又慢了一步,到底是不是呢,测试一下:

var fs = require("fs");
setTimeout(log.i, 0, "setTimeout1");
setImmediate(log.i, "immediate1");
setImmediate(log.i, "immediate2");
setImmediate(log.i, "immediate3");
setTimeout(log.i, 100, "setTimeout2");
fs.stat('test.js', log.i);
log.j(200);
log.i("end");

log输出:

[36 10:625]end
[36 10:796]setTimeout1
[36 10:796]setTimeout2
[36 10:796]immediate1
[36 10:796]null
[36 10:796]immediate2
[36 10:796]immediate3

其中输出null的那一行就是fs的,fs读文件是需要时间的,而且只有在end之后才执行,即使这样还是跑到两个Immediate前面。


---------------------------------------------------------------------------

下面的话题终于可以跟node.js沾上边了:

process.nextTick(function)

这个方法是node.js独有的,粗糙的意思就是,有空就把function执行一下,其作用跟setImmediate类似。不知道它的执行优先级有没有马上immediate高:

var fs = require("fs");
setTimeout(log.i, 0, "setTimeout1");
setImmediate(log.i, "immediate1");
process.nextTick(function(){log.i("nextTick1");});
setImmediate(log.i, "immediate2");
setImmediate(log.i, "immediate3");
setTimeout(log.i, 100, "setTimeout2");
fs.stat('test.js', log.i);
process.nextTick(function(){log.i("nextTick2");});
log.j(200);
log.i("end");

log输出:

[43 13:828]end
[43 14:47]nextTick1
[43 14:47]nextTick2
[43 14:47]setTimeout1
[43 14:47]setTimeout2
[43 14:47]immediate1
[43 14:47]null
[43 14:47]immediate2
[43 14:47]immediate3
优先度最高,这才是真正的马上。

至于为什么还要增加这么一个方法,查了一下,说的还是很清楚的:

node.js 0.9之后,任何异步递归都应用setImmediate而非process.nextTick!setImmediate和process.nextTick的区别是:前者将defer到队列末,且不会生成call stack;而后者是defer到该函数结束后执行,且process.nextTick用于递归会报警并最后爆栈...至于setInterval(func,0)就别用了,那是浏览器技巧。


介绍完这几个函数,异步编程应该有点懵懵懂懂的感觉了吧。

单线程实现多任务,异步是核心。


0 0