setTimeout和setInterval

基本知识

  • setTimeout(fn,t),超时调用,超过时间t,就执行fn。
  • setInterval(fn,t),间歇调用,调用周期t,执行fn。
    二者调用后,均返回一个数值ID,此ID是计划执行代码的唯一标识符,可以通过它来取消尚未执行的调用
    clearTimeout(id)clearInterval(id)取消间歇调用的重要性要远远高于取消超时调用,因为在不加干涉的情况下,间歇调用将会一直执行到页面卸载。

重点

JS是一个单线程的解释器,因此一段时间内只能执行一段代码。为了要控制执行的代码,就有一个JS任务队列。这些任务会按照将它们添加到队列的顺序执行。
1、setTimeout(fn,t)中t告诉JS再过多久把当前任务添加到队列中。并不是执行的到setTimeout就添加任务。如果队列是空,那么添加的代码会立即执行;如果队列不空,那么它就要等前面的代码执行完了以后在执行。
2、setInterval(fn,t),在间歇调用时,会出现一些意外。

setInterval(function () {
    func(i++);
}, 100)

每100毫秒调用一次func函数,如果func的执行时间少于100毫秒的话,在遇到下一个100毫秒前就能执行完:


问题1:但是,当func的执行时间多于100毫秒,在触发下一个func函数时(200ms处),上一个func还没有执行完,那么此时第二个func会在队列(event loop)中等待,直到第一个函数执行完,第二个func立即执行。这样就导致func执行间隔不再是100毫秒。
图片.png

var y=0;
var x = new Date().getTime();
var d=setInterval(a,500);
function a() {
    y++;
   sleep(1000);
    if(y>=4){
        clearInterval(d)
    }
console.log(new Date().getTime()-x);

}
function sleep(sleepTime){
    var start=new Date().getTime();
    while(true){
        if(new Date().getTime()-start>sleepTime){
            break;    
        }
    }
}

控制台输出


500毫秒后,第一次调用a函数,执行1000毫秒,结束时一共是1500毫秒,第二次a函数立即执行花费了1000毫秒,并没有在间隔500毫秒。
问题2:当第一个函数的执行时间特别长,以致于在执行过程中出发了多个func时,就会导致第3个func及以后的函数被忽视。因为任务队列中不会有两个及以上同一个定时器的回调函数

func的执行时长多于400毫秒,那么在200毫秒处,触发第二个func,第一个没执行完成,之后将第2个添加到空队列中,在300毫秒时,触发第3个func但是任务队列中已经有了第二个func,将第3个func抛弃掉。

优化

如何保证每次执行的间隔一样呢,可以使用setTimeout代替setInterval。例如每隔500ms执行一次b

var y=0;
var z = new Date().getTime();
var t=setTimeout(b,500);
function b(){
    y++;
   sleep(2000)
     if(y>=10){
        clearTimeout(t)
    }else{
        setTimeout(arguments.callee,500);
    }
     console.log(new Date().getTime()-z);
}
function sleep(sleepTime){
    var start=new Date().getTime();
    while(true){
        if(new Date().getTime()-start>sleepTime){
            break;    
        }
    }
}

控制台输出


b函数的执行时长2000毫秒,每隔500毫秒执行一次,那也就是两个b的执行结束的间隔是2500毫秒。和控制台输出的一致。
简写

var timer = setTimeout(function() { 
   执行内容....
    timer = setTimeout(arguments.callee, 2000)//再次调用
  }, 2000)

一般来说,使用超时调用是模拟间歇调用的一种最佳模式,在开发环境下很少用间歇调用

setTimeout除了做定时器外还能干什么用?

非常多,比如说:在处理DOM点击事件的时候通常会产生冒泡,正常情况下首先触发的是子元素的handler,再触发父元素的handler,如果我想让父元素的handler先于子元素的handler执行应该怎么办?那就用setTimeout延迟子元素handler若干个毫秒执行吧。问题是这个“若干个”毫秒应该是多少?可以是0。请看:

(function () {
    setTimeout(function () {
        alert(2);
    }, 0);

    alert(1);
})()  

会先弹出1,再弹出2
setTimeout,setInterval都存在一个最小延迟的问题,虽然你给的delay值为0,但是浏览器执行的是自己的最小值。HTML5标准是4ms,但并不意味着所有浏览器都会遵循这个标准,包括手机浏览器在内,这个最小值既有可能小于4ms也有可能大于4ms。在标准中,如果在setTimeout中嵌套一个setTimeout, 那么嵌套的setTimeout的最小延迟为10ms

setTimeout和线程的一些关系

var tbody = document.getElementsByTagName("tbody")[0];
for (var i = 0; i < 20000; i++) {
    var tr = document.createElement("tr");
    for (var t = 0; t < 6; t++) {
        var td = document.createElement("td");
        td.appendChild(document.createTextNode(i + "," + t));
        tr.appendChild(td);
    }
    tbody.appendChild(tr);
}

var t2 = +new Date();
console.log(t2 - t1); 

会发现先打印出时间,再渲染页面。
因为Javascript是单线程的(这里不谈web worker),也就是说浏览器无论什么时候都只有一个JS线程在运行JS程序。或许是因为单线程的缘故,也同时因为大部分触发的事件是异步的,JS采用一种队列(event loop)的机制来处理各个事件,比如用户的点击,ajax异步请求,所有的事件都被放入一个队列中,然后先进先出,逐个执行。
另一方面,浏览器还有一个GUI渲染线程,当需要重绘页面时渲染页面。但问题是GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
所以,在脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.
计算正确时间

var t1 = +new Date();

var tbody = document.getElementsByTagName("tbody")[0];
for (var i = 0; i < 20000; i++) {
    var tr = document.createElement("tr");
    for (var t = 0; t < 6; t++) {
        var td = document.createElement("td");
        td.appendChild(document.createTextNode(i + "," + t));
        tr.appendChild(td);
    }
    tbody.appendChild(tr);
}

setTimeout(function () {
    var t2 = +new Date();
    console.log(t2 - t1);   
}, 0)

设置一个setTimeout,腾出js引擎,让GUI线程渲染,渲染完后打印时间。
下面是一个看见颜色改变和更改div宽度的例子。使用的就是setTimeout,腾出JS引擎,让GUI渲染。

//js
 var div = document.getElementsByTagName('div')[0];
var style=document.defaultView.getComputedStyle(div,null);
console.log(style.width)
width=parseInt(style.width.slice(0,-2));
// console.log(typeof width)
 var i=0xbf0000;
function run() {
  width=parseInt(style.width.slice(0,-2));

    i+=0xfff0;
    if(i>=0xf00000){
        clearTimeout(t)
    }else{
         div.style.backgroundColor = '#'+i.toString(16) ;  
         div.style.width=width+10+'px';
        t=setTimeout(arguments.callee,100)
    }
    // console.log(i.toString(16))
   
}
t=setTimeout(run,100)

补充,常见的面试题

1、分析输出

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}

console.log(new Date, i);

输出:5->5,5,5,5,5(->代表1s)

2018-08-14T08:20:05.694Z 5
2018-08-14T08:20:06.696Z 5
2018-08-14T08:20:06.697Z 5
2018-08-14T08:20:06.697Z 5
2018-08-14T08:20:06.698Z 5
2018-08-14T08:20:06.698Z 5

2、追问 1:闭包
期望代码的输出变成:5 -> 0,1,2,3,4,该怎么改造代码?
方式一:

for (var i = 0; i < 5; i++) {
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

console.log(new Date, i);

方式二:

for (var i = 0; i < 5; i++) {
    setTimeout(
        (function (j) {  // j = i
            return function () {
                console.log(new Date, j);
            }
        })(i), 1000);
}

console.log(new Date, i);

存在错误的方式三

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}

console.log(new Date, i);//let,for循环外部访问不到i

追问 2:ES6
如果期望代码的输出变成 0 -> 1 -> 2 -> 3 -> 4 -> 5,并且要求原有的代码块中的循环和两处 console.log 不变,该怎么改造代码?
方式一:

for (var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000 * j);  // 这里修改 0~4 的定时器时间
    })(i);
}

setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
    console.log(new Date, i);
}, 1000 * i);

方式二:promise

var p= Promise.resolve()
for (var i = 0; i < 5; i++) {
    (function(j){
       p=p.then(setTimeout(function() {//p=p.then()相当于实现链式操作
            console.log(new Date, j);
        }, 1000*j ));  // 这里修改 0~4 的定时器时间
    })(i)
   
}

setTimeout(function(){
    p.then(console.log(new Date, i))
}, 1000*i) ;

方式二:promise.all

const tasks = [];
for (var i = 0; i < 5; i++) {   // 这里 i 的声明不能改成 let,如果要改该怎么做?
    ((j) => {
        tasks.push(new Promise((resolve) => {
            setTimeout(() => {
                console.log(new Date, j);
                resolve();  // 这里一定要 resolve,否则代码不会按预期 work
            }, 1000 * j);   // 定时器的超时时间逐步增加
        }));
    })(i);
}

Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);   // 注意这里只需要把超时设置为 1 秒
});

坑点

for (var i = 0; i < 5; i++) {
    setTimeout((function(i) {
      console.log(i);
    })(i), i * 1000);
  }

setTimeout的第一个参数可以是字符串也可以是函数,但是不建议是用字符串。
上述代码在node下运行出错:throw new TypeError('"callback" argument must be a function');,但是在浏览器下立即输出01234。因为参数函数是立即调用,当时间过去,到了应该执行函数的时候,只有undefined了等价于setTimeout(undefined, ...);

参考资料:

1、js高级程序设计第3版
2、http://qingbob.com/difference-between-settimeout-setinterval/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容