提到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)就别用了,那是浏览器技巧。
介绍完这几个函数,异步编程应该有点懵懵懂懂的感觉了吧。
单线程实现多任务,异步是核心。