- 参考资料
- 《JavaScript高级程序设计(第3版)》 22.3 高级定时器
- 相关链接
一、简述定时器的机制
看以下例子:
var btn = document.getElementById("my-btn");
btn.onclick = function(){
setTimeout(function(){
document.getElementById("message").style.visibility = "visible";
}, 250);
//其他代码
};
*上述代码的解析:
- 先给按钮设置了一个事件处理程序,事件处理程序里设置了一个
250ms
后调用的定时器。- 点击按钮后,先把
onclick
事件处理程序添加到任务队列中。- 该事件处理程序执行后,再设置定时器,
250ms
后,指定的代码才被添加到队列中等待执行。- 如果队列为空,即:没有其他代码等待执行,那么定时器内的代码就会执行; 如果队列不为空,即:有其他代码等待执行,那么定时器内的代码就会等到队列中的代码执行完后执行。
- 总结:
- 定时器只是用来计划代码在未来的某个时间执行。
- 定时器所指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。
- 在
js
中没有任何代码是立刻执行的,除非进程是空闲的,而不管它们是如何添加到队列中。
二、重复的定时器 - setInterval()
2.1 setInterval()问题的阐述
setInterval()
:每隔指定的时间就执行一次代码。说明了它创建的定时器确保了定时器代码规则地插入队列中。
- 但这个创建定时器的方式也存在问题:
- 定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。
JavaScript
引擎能避免这个问题:
- 当使用
setInterval()
时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。- 这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。
2.2 setInterval()问题展示的例子
setInterval()
问题:
- 某些间隔会被跳过。
- 多个定时器的代码执行之间的间隔可能会比预期的小。
- 先看以下例子:
- 假设,某个
onclick
事件处理程序使用setInterval()
设置了一个200ms
间隔的重复定时器。- 如果事件处理程序花了
300ms
多一点的时间完成,同时定时器代码也花了差不多的时间,就会同时出现跳过间隔且连续运行定时器代码的情况。
- 上图分析:
- 第 1 个定时器是在
205ms
处添加到队列中的,但是直到过了300ms
处才能够执行。- 当执行这个定时器代码时,在 405ms 处又给队列添加了另外一个副本。
- 在下一个间隔,即
605ms
处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器代码的实例。- 结果是,在这个时间点上的定时器代码不会被添加到队列中。
- 结果在
5ms
处添加的定时器代码结束之后,405ms
处添加的定时器代码就立刻执行。
2.2 setInterval()问题解决方法
- 解决上述两个问题的方法:用如下模式使用链式
setTimeout()
调用。
- 这个模式链式调用了
setTimeout()
,每次函数执行的时候都会创建一个新的定时器。- 第二个
setTimeout()
调用使用了arguments.callee
来获取对当前执行的函数的引用,并为其设置另外一个定时器。- 这样做的好处:
- 在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。
- 它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。
setTimeout(function(){
//处理中
setTimeout(arguments.callee, interval);
}, interval);
三、定时器应用 - Yielding Processes
3.1 长时间运行脚本的制约
- 长时间运行脚本的制约:如果代码运行超过特定的时间或者特定语句数量就不让它继续执行。
- 如果代码达到了这个限制,会弹出一个浏览器错误的对话框:告诉用户某个脚本会用过长的时间执行,询问是允许其继续执行还是停止它。
- 定时器是绕开此限制的方法之一。
3.2 脚本长时间运行问题产生的原因
- 脚本长时间运行的问题产生的原因:
- 过长的、过深嵌套的函数调用。
- 进行大量处理的循环。
3.3 数组分块技术(array chunking)
- 长时间运行的循环通常遵循以下模式:
for (var i=0, len=data.length; i < len; i++){
process(data[i]);
}
- 这个模式的问题在于 要处理的项目的数量在运行前是不可知 的。【 数组中的项目数量直接关系到执行完该循环的时间长度。】
- 例子:如果完成
process()
要花100ms
,只有2
个项目的数组可能不会造成影响,但是10
个的数组可能会导致脚本要运行一秒钟才能完成。- 由于
JavaScript
的执行是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间也越久。
- 在展开循环前,需先考虑以下两个问题:
- 该处理是否必须同步完成?
- 如果这个数据的处理会造成其他运行的阻塞,那么最好不要改动它。
- 如果你对这个问题的回答确定为“否”,那么将某些处理推迟到以后是个不错的备选项。
- 数据是否必须按顺序完成?
- 通常,数组只是对项目的组合和迭代的一种简便的方法而无所谓顺序。
- 如果项目的顺序不是非常重要,那么可能可以将某些处理推迟到以后。
- 当你发现某个循环占用了大量时间,同时对于上述两个问题,你的回答都是“否”,那么你就可以使用定时器分割这个循环。
- 这是一种叫做数组分块(
array chunking
)的技术,小块小块地处理数组,通常每次一小块。
- 基本的思路:为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。
function chunk(array, process, context){
setTimeout(function(){
// 取出下一个条目并处理
var item = array.shift();
// 通过 call()调用的 process()函数,这样可以设置一个合适的执行环境(如果必须)
process.call(context, item);
// 若还有条目,再设置另一个定时器
if (array.length > 0){
// 可以根据你的需要更改这个间隔大小,不过 100ms 在大多数情况下效果不错。
setTimeout(arguments.callee, 100);
}
}, 100);
}
chunk(array, process, context)
函数分析:
chunk(array, process, context)
接受三个参数:
array
:要处理的项目的数组。process
:用于处理项目的函数。context
:可选的运行该函数的环境。- 在数组分块模式中,
array
变量本质上就是一个 “待办事宜”列表,它包含了要处理的项目。- 使用
shift()
方法可以获取队列中下一个要处理的项目,然后将其传递给某个函数。- 如果在队列中还有其他项目,则设置另一个定时器,并通过
arguments.callee
调用同一个匿名函数。
- 补充:
- 由于数组是引用类型,如果你想保持原数组不变,则应该将该数组的克隆传递给
chunk()
【即:某数组.concat()
】。- 数组分块的重要性在于它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其他的浏览器处理机会运行,这样就可能避免长时间运行脚本的错误。
- 一旦某个函数需要花
50ms
以上的时间完成,那么最好看看能否将任务分割为一系列可以使用定时器的小任务。
四、定时器应用 - 函数节流
4.1 函数节流介绍
- 浏览器中某些计算和处理要比其他的昂贵很多。
- 例如:
DOM
操作比起非DOM
交互需要更多的内存和CPU
时间。
- 连续尝试进行过多的
DOM
相关操作可能会导致浏览器挂起,有时候甚至会崩溃。- 尤其在
IE
中使用onresize
事件处理程序的时候容易发生,当调整浏览器大小的时候,该事件会连续触发。- 在
onresize
事件处理程序内部如果尝试进行DOM
操作,其高频率的更改可能会让浏览器崩溃。
- 为了绕开这个问题,你可以 使用定时器对该函数进行节流。
- 函数节流基本思想:某些代码不可以在没有间断的情况连续重复执行。
- 第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。
- 当第二次调用该函数时,它会清除前一次的定时器并设置另一个。
- 如果前一个定时器已经执行过了,这个操作就没有任何意义。
- 然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。
- 目的是只有在执行函数的请求停止了一段时间之后才执行。
- 函数节流基本模式:
var processor = {
timeoutId: null,
//实际进行处理的方法
performProcessing: function(){
//实际执行的代码
},
// 初始化任何处理所必须调用的
process: function(){
// 第一步是清除存好的 timeoutId,来阻止之前的调用被执行。
clearTimeout(this.timeoutId);
// 保存 this 的引用
var that = this;
this.timeoutId = setTimeout(function(){
// 创建一个新的定时器调用 performProcessing()
that.performProcessing();
}, 100);
}
};
//尝试开始执行
processor.process();
- 时间间隔设为了
100ms
,这表示最后一次调用process()
之后至少100ms
后才会调用performProcessing()
。 - 所以如果
100ms
之内调用了process()
共 20 次,performanceProcessing()
仍只会被调用一次。
4.2 throttle()函数
-
使用
throttle()
函数来简化上述模式,这个函数可以自动进行定时器的设置和清除。 -
throttle(fn, context)
:fn
::要执行的函数。context
:函数在哪个作用域中执行。
function throttle(method, context) {
clearTimeout(method.tId);
method.tId= setTimeout(function(){
method.call(context);
}, 100);
}
- 上述代码分析:
- 首先清除之前设置的任何定时器。
- 定时器
ID
是存储在函数的tId
属性中的,第一次把方法传递给throttle()
的时候,这个属性可能并不存在。- 接下来,创建一个新的定时器,并将其
ID
储存在方法的tId
属性中。- 如果这是第一次对这个方法调用
throttle()
的话,那么这段代码会创建该属性。- 定时器代码使用
call()
来确保方法在适当的环境中执行。- 如果没有给出第二个参数,那么就在全局作用域内执行该方法。
4.3 throttle()的应用
- 节流在
resize
事件中是最常用的。- 如果你基于该事件来改变页面布局的话,最好控制处理的频率,以确保浏览器不会在极短的时间内进行过多的计算。
- 例如,假设有一个
div
元素需要保持它的高度始终等同于宽度。
window.onresize = function(){
var div = document.getElementById("myDiv");
div.style.height = div. offsetWidth + "px";
};
- 上述代码分析:
- 这段非常简单的例子有两个问题可能会造成浏览器运行缓慢。
- 首先,要计算
offsetWidth
属性,如果该元素或者页面上其他元素有非常复杂的CSS
样式,那么这个过程将会很复杂。- 其次,设置某个元素的高度需要对页面进行回流来令改动生效。【如果页面有很多元素同时应用了相当数量的
CSS
的话,这又需要很多计算。】
- 解决方法 –
throttle()
function resizeDiv(){
var div = document.getElementById("myDiv");
div.style.height = div.offsetWidth + "px";
}
window.onresize = function(){
throttle(resizeDiv);
};
补充:只要代码是周期性执行的,都应该使用节流,但是你不能控制请求执行的速率。这里展示的 throttle()函数用了 100ms 作为间隔,你当然可以根据你的需要来修改它。