介绍
JavaScript
提供了两个方法供我们设置一个定时器,它们分别是 setTimeout()
和 setInterval()
。这两种方法的使用方法是相同的,都接收两个参数,第一个参数是一个回调函数,第二个参数是延迟的毫秒数。这就造成了一种 JavaScript
是多线程语言的假象,因为相同的功能在 Java
中称之为 sleep()
或者 Lock.lock()
,它们的作用都是堵塞当前线程,为其他线程腾出处理器资源。
实际上 JavaScript
是运行在单线程环境中的,它拥有一个事件处理队列,所有要处理的事件都会被放置在这个队列中排队等待执行。这样的话就出现了一个问题,浏览器并不能保证我们的代码会在指定的时间内执行。
举个例子来说,如果一个事件的执行时间非常长,那么在这个时间的执行过程中,我们点击页面上的任何按钮或者其他可点击控件,都无法得到回馈,因为我们的点击事件正在队列中排队执行。
从上面的介绍中,我们明白了一个道理,那就是不能让一个事件处理时间过长,否则就会导致用户无法与页面进行体验良好的交互。所以,现在有很多技巧用于处理耗时操作,比如函数节流和分块处理,接下来我会讲解这些技巧。
两种方法的比较
setTimeout()
的作用是在指定时间内执行一个任务。setInterval()
的作用是以指定时间周期性的执行任务。
按理说,这两种方法分工明确,我们应该根据自身的需要选择使用 setTimeout()
或者 setInterval()
,但是目前的最佳实践却是始终使用 setTimeout()
,即在应该使用 setTimeout()
的时候使用 setTimeout()
, 在应该使用 setInterval()
的时候用 setTimeout()
去替代。
原因就是使用 setInterval()
的时候会出现间隔跳过问题。比如我们设置了一个 setInterval(callback, 10)
,如果这个 callback
的执行时间是 20ms,那么就会出现无间隔连续执行 callback
的情况,不过 JavaScript
引擎处理的过程却不和我们想象的一样,如果当前事件队列中已经有定时器代码实例了,它就不会再放一个相同的定时器进去。这也就导致一部分定时器会被跳过的问题。
下面是利用 setTimeout()
代替 setInterval()
的例子。
function callback() {
console.log("Hello World!");
}
setInterval(callback, 10);
// After
function callback() {
console.log("Hello World");
setTimeout(callback, 10);
}
setTimeout(callback, 10);
使用了 setTimeout()
之后,可以保证在一个定时器任务执行之后才会再次将一个定时器插入队列,不会有丢失间隔的问题。
高级技巧
- 分块处理
导致脚本长时间运行的两个主要原因就是过深过长的嵌套函数调用和包含大量处理过程的循环。对于后一个问题,我们可以对循环进行切割,分时处理,腾出时间为其他事件进行服务。
function chunk(array, process, context) {
setTimeout(function() {
var item = array.shift();
process.call(context, item);
if(array.length > 0) {
setTimeout(arguments.callee, 100);
}
}, 100);
}
var data = [1, 2, 3, 4, 5, 6];
function printValue(i) {
console.log(i);
}
chunk(data, printValue);
可见,在以上代码中我们以 100ms 的间隔去执行打印事件,这样的话在间隔的过程中,浏览器就能很好的处理与用户的交互。在执行耗时任务时,这一技巧十分重要,因为网页的明显卡顿会让你的用户离你而去。
-
函数节流
因为JavaScript
是在浏览器中执行的,所以它的限制非常大,这也就迫使我们去考虑代码的性能以及资源的利用问题。函数节流的思想就是,某些代码不可以在没有间隔的情况下连续执行。举个例子来说,如果用户疯狂的点击页面中的一个按钮,频率非常之高,这个时候就不能按照用户点击的次数去调用处理程序。难道用户一秒点击 20 次按钮我们还要重复的执行 20 次处理程序吗?这是完全没有必要的,所以我们可以采取setTimeout()
让用户请求结束后一段时间再去执行。var clickButton = document.getElementById("click"); function throttle(method, context) { clearTimeout(method.tId); method.tId = setTimeout(function() { method.call(context); },2000); } function print() { console.log("You click the button!"); } clickButton.onclick = function() { throttle(print); }
以上代码就保证了在 2s 之内无论你点击了多少次按钮,事件处理函数只会执行一次,当然设置 2s 只是试验性的,在实际开发过程中,2s 可能太长了,需要改成一个较小的值,比如说 100ms 。
- 中央定时器控制
如果我们同时创建了大量的定时器,将会在浏览器中增加垃圾回收任务发生的可能性。所以为了避免我们的定时器被当做垃圾回收掉,可以使用中央定时器控制的技术。下面是中央定时器控制的一些特点:
每个页面在同一时间只需要运行一个定时器。
可以根据需要暂停和恢复定时器。
-
删除回调函数的过程变得简单。
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; } };
上述代码就创建了一个简单的中央定时器,首先我们在 timers 中定义了 timerId 和 timers, timerId用于保存定时器的 id,用于控制定时器的开启和关闭,timers 用于保存要执行的函数。
最主要的还是 start
函数,首先对 timerId
进行判断,如果还没有启动定时器,则立即启动一个,如果已经有定时器启动了,则直接返回,用于确保整个环境中只存在一个定时器实例。每次执行定时器实例,都会执行一次保存在 timers
队列中的函数,如果执行的函数返回结果为 false
, 就会将其从队列中删除,这样下次就不会再执行该函数了。
End!