JS setTimeout()与setInterval()的区别

JS setTimeout()setInterval()的区别

  • setTimeout()setInterval()的基本用法我们一带而过:
  1. 指定延迟后调用函数,
  2. 以指定周期调用函数

setInterval()函数

setInterval(function() {
    func(i++);
}, 100)
  • 每隔100毫秒调用一次func函数,如果func的执行时间少于100毫秒的话,在遇到下一个100毫秒前就能够执行完:

func的执行时间少于100毫秒

  • 但如果func的执行时间大于100毫秒,该触发下一个func函数时之前的还没有执行完怎么办?
    答案如下图所示,那么第二个func会在队列(这里的队列是指event loop)中等待,直到第一个函数执行完

func的执行时间大于100毫秒

  • 如果第一个函数的执行时间特别长,在执行的过程中本应触发了许多个func怎么办,那么所有这些应该触发的函数都会进入队列吗?
  • 答案肯定是不会的,只要发现队列中有一个被执行的函数存在,那么其他的统统被忽略。如下图,在第300毫秒和400毫秒处的回调都被抛弃,一旦第一个函数执行完后,接着执行队列中的第二个,即使这个函数已经“过时”很久了。

队列中的处理情况

  • 还有一点,虽然在setInterval()函数里指定的周期是100毫秒,但它并不能保证两个函数之间调用的间隔一定是100毫秒。在上面的情况中,如果队列中的第二个函数是在第450毫秒处结束的话,在第500毫秒时,它会继续执行下一轮func,也就是说这之间的间隔只有50毫秒,而非周期100毫秒。
  • 那如果想保证每次执行的间隔应该怎么办?用setTimeout函数。

setTimeout()函数

var i = 1;
var timer = setTimeout(function() {
    alert(i++);
    timer = setTimeout(arguments.callee, 2000);
}, 2000);
  • 上面的函数每2秒钟递归调用自己一次,可以在某一次alert的时候等待任意长的时间(不按"确定"按钮),接下来无论什么时候点击"确定",下一次执行一定离这次确定相差2秒钟的。
  • 下面上下两段代码虽然看上去功能一致,但实际并非如此,原因就是我上面所说
setTimeout(function repeatMe() {
    /* Some long block of code... */
    setTimeout(repeatMe, 10);
}, 10);

setInterval(function() {
    /*Some long block of code...  */
}, 10);

setTimeout()函数除了做定时器外还能干什么用?

  • setTimeout()函数当然还有其他非常多的作用。比如说: 在处理DOM点击事件的时候通常会产生冒泡,正常情况下首先触发的是子元素的handler,再触发父元素的handler,如果想让父元素的handler先于子元素的handler执行应该怎么办?那就用setTimeout延迟子元素handler若干个毫秒执行吧。问题是这个"若干个"毫秒应该是多少?可以是0。
  • 你可能会疑惑如果是0的话那不是立即执行了吗?不,看一下下面的代码:
(function() {
    setTimeout(function() {
        alert(2);
    }, 0);
    
    alert(1);
})()
  • 先弹出的应该是1,而不是"立即执行"的2。
  • setTimeout()setInterval()都存在一个最小延迟的问题,虽然你给的delay值为0,但是浏览器执行的是自己的最小值。HTML5标准是4ms,但并不意味着所有浏览器都会遵循这个标准,包括手机浏览器在内,这个最小值既有可能小于4ms也有可能大于4ms。在标准中,如果在setTimeout()中嵌套一个setTimeout(),那么嵌套的setTimeout()的最小延迟为10ms。

聊聊setTimeout线程的一些关系

  • 现在我有一个非常耗时的操作(如下面的代码,在table中插入20000行),我想计算这个操作所消耗的时间应该怎么办?你觉得下面这个用new Date来计算的方法怎么样:
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);
}

var t2 = +new Date();
console.log(t2 - t1);
  • 如果你尝试运行起来就会发现问题,在这20000行还没有渲染出来的时候,控制台就已经打印出来了时间,这两个时间差并非误差所致(可能这个操作需要5秒,甚至10秒以上),但是打印出来的时间只有1秒左右,这是为什么?
  • 因为Javascript是单线程的(这里不谈web worker),也就是说浏览器无论什么时候都只有一个JS线程在运行JS程序。或许是因为单线程的缘故,也同时因为大部分触发的事件是异步的,JS采用一种队列(event loop)的机制来处理各个事件,比如用户的点击,ajax异步请求,所有的事件都被放入一个队列中,然后先进先出,逐个执行。这也就解释了开头setInterval的那种情况。
  • 另一方面,浏览器还有一个GUI渲染线程,当需要重绘页面时渲染页面。但问题是GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  • 所以,上面的那个例子中算出的时间只是javascript执行的时间,在这之后,GUI线程才开始渲染,而此时计时已经结束了。那么如何你能计算出正确时间了?在结尾添加一个setTimeout()函数。
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)
  • 这样能让操纵DOM的代码执行完后不至于立即执行t2 - t1,而在中间空闲的时间恰好允许浏览器执行GUI线程。渲染完之后,才计算出时间。

Example

function run() {
    var div = document.getElementsByTagName('div')[0];
    for(var i = 0xA00000; i < 0xFFFFFF; i++) {
        div.style.backgroundColor = '#' + i.toString(16);
    }
}
  • setInterval()函数有一个很重要的应用是javascript中的动画。
  • 举个例子,假设我们有一个正方形div,宽度为100px,现在想让它的宽度在1000毫秒内增加到300px——很简单,算出每毫秒内应该增加的像素,再按每毫秒为周期调用setInterval()实现增长.
var div = $('div')[0];
var width = parseInt(div.style.width, 10);

var MAX = 300, duration = 1000;
var inc = parseFloat((MAX - width)/duration);

function animate(id) {
    width += inc;
    if (width >= MAX) {
        clearInterval(id);
        console.timeEnd("animate");
    }
    div.style.width = width + "px";
}

console.time("animate");
var timer = setInterval(function () {
    animate(timer);
}, 0);
  • 代码中利用console.time()来计算时间所花费的时间——实际上花的时间是明显大于1000毫秒的,why? 因为上面说到最小周期至少应该是4ms,所以每个周期的增长量应该是每毫秒再乘以4。
var inc = parseFloat((MAX - width)/duration)*4;
  • 如果你有心查看Jquery的动画源码的话,你能发现源码的时间周期是13ms,这是我不解的地方——如果追求流畅的动画效果来说,每秒(1000毫秒)应该是60帧,这样算下来每帧的时间应该是16.7毫秒,在这里我把每帧定义为完成一个像素增量所花的时间,也就是16毫秒(毫秒不允许存在小数)是让动画流畅的最佳值。所以Jquery的这个13毫秒值是如何来的了?
  • 无论如何优化setInterval(),误差是始终存在的。但其实在HTML5中,有一个实践动画的最佳途径requestAnimationFrame。这个函数能保证能以每帧来执行动画函数。比如上面的例子可以改写为:
// init some values
var div = $('div')[0].style;
var height = parseInt(div.height, 10);

// calc distance we need to move per frame over a time
var max = 300;
var steps = (max - height)/seconds/16.7;

// 16.7ms is approx one frame(1000/60)

// loop
function animate(id) {
    height += steps; // use calculated steps
    div.height = height + "px";

    if(height < max) {
        requestAnimationFrame(animate);
    }
}

animate();
  • 关于这个函数和它对应的cancel函数,或者是polyfill就不在这延伸了,如果你有什么见解关于这部分的可以留言。
  • 这种情况下通常会有多个计时器同时运行,如果同时大量计时器同时运行的话,会引起一些个问题,比如如何回收这些计时器?Jquery的作者John Resig建议建立一个管理中心,它给出的一个非常简单的代码如下:
var timers = {
    timerID: 0,
    timers: [],
    add: function(fn) {
        this.timers.push(fn);
    },
    start: function() {
        if (this.timerID) return;
        (function runNext() {
            if (timers.timers.length > 0) {
                for(var i = 0; i < timers.timers.length; i++) {
                    if(timers.timers[i]() === false) {
                        timers.timers.splice(i, 1);
                        i--;
                    }
                }
                timers.timerID = setTimeout(runNext, 0);
            }
        })();
    },
    stop: function() {
        clearTimeout(this.timerID);
        this.timerID = 0;
    }
};
  • 注意看中间的start方法: 他把所有的定时器都存在一个timers队列(数组)中,只要队列长度不为0,就轮询执行队列中的每一个子计时器,如果某个子计时器执行完毕(这里的标志是返回值是false),那就把这个计时器踢出队列。继续轮询后面的计时器。
  • 上面描述的整个一轮轮询就是runNext,并且递归轮询,一遍一遍的执行下去timers.timerID = setTimeout(runNext, 0)直到数组为空。
  • 注意到上面没有使用到stop方法,Jquery的动画animate就是使用的是这种机制,不过更完善复杂,摘一段Jquery源码看看,比如就类似的runNextt这段:
// /src/effects.js:674
jQuery.fx.tick = function() {
    var timer,
        timers = jQuery.timers,
        i = 0;
    
    fxNow = jQuery.now();
    
    for(; i < timers.length; i++) {
        timer = timers[i];
        // Checks the timer has not already been removed
        if(!timer() && timers[i] === timer) {
            timers.splice(i--, 1);
        }
    }

    if(!timers.length) {
        jQuery.fx.stop();
    }
    fxNow = undefined;
};

// /src/effects.js:703
jQuery.fx.start = function() {
    if(!timerId) {
        timerId = setInterval(jQuery.fx.tick, jQuery.fx.interval);
    }
};
  • 不解释,和上面的那段已经非常类似了,有兴趣的亲们可以在github上阅读整段effect.js代码。
  • 最后setTimeout()函数的应用就是总所周知,来处理因为js处理时间过长造成浏览器假死的问题了。这个技术在《JavaScript高级程序设计》中已经阐述过了。简单来说,如果你的循环:
  1. 每一次处理不依赖上一次的处理结果;
  2. 没有执行的先后顺序之分;
function chunk(array, process, context) {
    setTimeout(function() {
        var item = array.shift();
        process.call(context, item);
        if(array.length > 0) {
            setTimeout(arguments.callee, 100);
        }
    }, 100);
}
  • chunk()函数的用途就是将一个数组分成小块处理,它接受三个参数: 要处理的数组,处理函数以及可选的上下文环境。每次函数都会将数组中第一个对象取出交给process()函数处理,如果数组中还有对象没有被处理则启动下一个timer,直到数组处理完。这样可保证脚本不会长时间占用处理机,使浏览器出一个高响应的流畅状态。

JackDan9 Thinking

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值