深入解析JavaScript定时器原理与优化策略

作者:性能诊所时间:2026-10-28 19:13:28浏览:476

本文深入探讨JavaScript单线程模型与浏览器多线程环境的协作机制,解析Event Loop如何调度异步任务。详细对比setTimeout与setInterval的执行差异,分析定时器精度不准的根本原因,并提供防抖、节流及多定时器合并等实用优化方案,帮助开发者编写更高效的异步代码。

JavaScript单线程与Event Loop机制

JavaScript采用单线程模型运行。

JavaScript被设计为浏览器脚本语言,其核心职责在于与用户进行交互以及操作DOM元素。这种应用场景决定了它必须采用单线程模式,否则将引发复杂的同步冲突问题。例如,若存在两个线程同时操作DOM,一个线程负责添加内容,另一个线程负责删除该节点,浏览器将无法确定应以哪个线程的操作结果为准。因此,为了避免此类复杂性,JavaScript从诞生之初就确立了单线程的核心特征,且这一设计在可预见的未来不会改变。

任务队列机制

由于单线程的限制,所有任务必须按顺序排队执行。只有当前面的任务执行完毕后,后续的任务才会开始执行。如果前一个任务耗时过长,后续任务将不得不一直等待。

异步事件驱动模型

浏览器中的许多行为是异步的,例如鼠标点击、窗口大小调整、定时器触发以及XMLHttpRequest完成回调等。当异步事件发生时,相应的回调函数会被放入事件队列中。浏览器内部维护着一个名为Event Loop(事件循环)的大消息循环,它会不断轮询事件队列并处理其中的事件。例如,当浏览器正在处理onclick事件时,若发生window onSize事件,该异步事件会被放入队列等待,直到前一个任务处理完毕且JavaScript线程空闲时,才会执行该事件。

Event Loop机制详解

JavaScript引擎是单线程的,但浏览器整体并非单线程。

浏览器内部至少包含以下进程:

1. 浏览器GUI渲染线程

2. JavaScript引擎线程

3. 浏览器定时器触发器线程

4. 浏览器事件触发线程

5. 浏览器HTTP异步请求线程

由于JavaScript引擎是单线程的,代码执行时会先被压入队列,然后由引擎按照先进先出的顺序运行。事件处理函数和定时器执行函数也会被排入该队列,并通过一个无限循环不断从队头取出函数执行,这就是Event Loop的工作方式。

总结来说,JavaScript是单线程的,但浏览器是多线程的。遇到异步操作时,由浏览器将异步回调放入Event Loop中,当JavaScript线程空闲时,再去读取并执行Event Loop中的任务。

定时器原理与精度分析

定时器原理

定时器的基本用法

setTimeout(fn, delay)

setInterval(fn, delay)

其中fn可以是函数或字符串,delay表示延迟时间,单位为毫秒。

使用定时器时需注意以下几点:

1. 虽然fn可以是字符串,但强烈不推荐这种做法。

2. 如果fn内部的函数涉及this关键字,执行时this将指向window对象。

如果理解了JavaScript的单线程模型和Event Loop机制,那么定时器的原理也就很容易理解了。

当设置了定时器,到达延迟时间后,浏览器会将延迟执行的事件放入Event Loop中。如果此时JavaScript线程空闲,事件就会被执行(因此定时器的精度并不准确)。

有文章介绍setTimeout和setInterval对函数轮询的区别,代码如下:

代码如下:

setTimeout(function(){
setTimeout(arguments.callee,100)
},100)
setInterval(function(){},1000)

文章的大意是,setTimeout是在回调函数执行后才启动下一次定时器,因此肯定是有间隔的执行;而setInterval是一直都在执行,如果碰到JavaScript线程一直繁忙,可能会在Event Loop中加入多个回调,当JavaScript线程不忙时,会一次性执行多个回调。

经过测试发现,无论在IE、Firefox、Chrome、Opera还是Safari浏览器下,setInterval都是按一定间隔执行的。

测试代码如下:

代码如下:

setInterval(function(){
xx.innerHTML=xx.innerHTML+1;
},100);
        
for(var i=0;i<6000000;i++){
xx.offsetWidth
        }
setTimeout(function(){
 debugger;
 },10)

断点的时候还是只打印出了1个1

定时器的精度问题

由于JavaScript是单线程的,如果遇到繁忙情况,定时器肯定不准,而且误差会越来越大。这似乎无解。

另一个精度问题是最小间隔setTimeout(fun,0)。

在JavaScript线程不忙的时候,也不可能0秒后马上执行,总有一个最小间隔,且每个浏览器各不相同,此处未做详细测试。

有文章称W3C标准规定定时器的最小时间执行是4ms,但找不到出处,无从考证。

定时器性能优化策略

定时器相关的一些优化

在使用定时器时,可以采取一些优化措施。

1. 例如绑定window.onresize事件,在浏览器缩放时触发非常频繁,因此可以延迟执行,下次执行时clear掉,以减少频繁执行。

伪代码如下:

代码如下:

var timer;
function r(){
clearTimeout(timer);
timer = setTimeout(function(){
//do something
},150);
    }

2. 在滚动条往下拉的时候也一样,例如图片的lazyload,也应该有一个定时器,避免过多的计算。

3. 当有多个地方需要定时器时,可以合并成一个定时器,时间间隔以最小的那个为准,然后需要执行的回调函数放入数组中,当到达时间间隔时,遍历数组执行即可。

一个小demo

代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
.wrap{width:80%; margin: 30px auto; border: 1px solid #ccc; padding: 20px;}
.c{border: 1px solid #ccc; height: 30px;margin-bottom: 20px;}
</style>
</head>
<body>
<div id="xx"></div>
<div class="wrap" >
<div id="a1" class="c">0</div>
<div id="a2" class="c">0</div>
<div id="a3" class="c">0</div>
<div id="a4" class="c">0</div>
</div>
<script src="http://static.paipaiimg.com/paipai_h5/js/ttj/zepto.min.js"></script>
<script type="text/javascript">
var runTime = {
options : {
step : 1000
            },
callbacks:[],
addCallbacks : [],
start : false,
timer : null,
extend : function(){
var target = arguments[0] || {}, i = 1, length = arguments.length, options;
if ( typeof target != "object" && typeof target != "function" )
target = {};
for ( ; i < length; i++ )
if ( (options = arguments[ i ]) != null )
for ( var name in options ) {
var copy = options[ name ];
if ( target === copy)
continue;
if ( copy !== undefined )
target[ name ] = copy;
                        }
return target;
            },
init : function(options){
$.extend(this,this.options,options||{});
            },
add : function(fun,options){
options = options ||{};
this.addCallbacks.push({
fun : fun,
startTime : new Date().getTime(),
step : options.step || this.step,
i : 1
                });
var self = this;
if(!this.start){
this.callbacks = [fun];
this.start = true;
this.startTime = new Date().getTime();
this.timer = setInterval(function(){
self.done();
                       
},this.step);
                }
            },
done : function(){
var callbacks = this.callbacks,
self = this,
newArr = [];
$.each(callbacks,function(i,obj){
if(obj.step == self.step){
obj.fun();
}else{
if(obj.i == obj.step/self.step){
if((new Date().getTime())-obj.startTime>obj.step*2/3){
obj.fun();
                            }
obj.i = 1;
}else{
obj.i = obj.i + 1;
                        }
                    }
                });
$.each(this.addCallbacks,function(i,obj){
if(obj.step == self.step){
if((new Date().getTime())-obj.startTime>obj.step*2/3){
obj.fun();
callbacks.push(obj);
}else{
newArr.push(obj);
                        }
}else{
obj.i = obj.i + 1;
callbacks.push(obj);
                    }
                });
this.addCallbacks = newArr;
            },
clear : function(){
clearInterval(this.timer);
            }
        }
runTime.init();
runTime.add(function(){
 a1.innerHTML = ~~a1.innerHTML+1;
 });
runTime.add(function(){
 a2.innerHTML = ~~a2.innerHTML+1;
 },{step:2000});
runTime.add(function(){
 a3.innerHTML = ~~a3.innerHTML+1;
 },{step:4000});
 runTime.add(function(){
 a4.innerHTML = ~~a4.innerHTML+1;
 },{step:8000});
 </script>
</body>
</html>

小伙伴们是否对javascript定时器有所了解了呢,如有疑问给我留言吧。

作者最新文章