freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

js基础之setTimeout与setInterval原理分析
FreeBuf-357997 2023-05-25 15:18:55 131577
所属地 北京

setTimeout与setInterval概述

setTimeout与setInterval是JavaScript引擎提供的两个定时器方法,分别用于函数的延时执行和循环调用。前者的主要思想是通过一个定时器,让函数在计时结束后再执行;后者则是每隔一定的时间,就启动一次函数的执行。

从原理来看,两者似乎并不复杂。但由于JavaScript引擎是单线程的,这就让上述两个定时器的实际执行变得稍微复杂了一些。下面我们来看一下两者的运行机制与需要注意的问题。

基本原理

知识铺垫

单线程模型:由于JavaScript被设计为用在浏览器环境,而该环境下存在大量可能发生冲突的DOM操作,为了避免进行复杂的冲突处理(可能存在的冲突数量几乎不可预测),JavaScript的设计者舍弃了java的多线程模型(该模型下,执行引擎同时可以做几件事,但要进行线程同步),将其设计成了一门单线程语言(执行引擎在同一时间只做一件事)。

注意:这里的单线程是指JavaScript的主线程只有一个。除了这个主线程,JavaScript还有一个I/O线程,通过事件循环来处理I/O问题,但两者之间相对独立,不需要进行状态同步,因此我们仍然可以把JavaScript看成一门单线程语言。

任务队列:所谓任务队列,就是用于存储等待执行的任务的队列。由于JavaScript是一门单线程语言,如果当前有一个任务需要执行,但JavaScript引擎正在执行其他任务,那么这个任务就需要放进一个队列中进行等待。等到线程空闲时,就可以从这个队列中取出最早加入的任务进行执行(类似于我们去银行排队办理业务。单线程相当于说这家银行只有一个服务窗口,一次只能为一个人服务,后面到的就需要排队,而任务队列就是排队区,先到的就优先服务)。

注意:如果当前线程空闲,并且队列为空,那每次加入队列的函数将立即执行。

setTimeout与setInterval

setTimeout(func, delay, args):设置超时调用。如对于setTimeout(func, 100, args),js引擎会为func函数设置一个计时器,100毫秒后,将func添加到任务队列等待执行。

setInterval(func, interval, args):设置循环调用。对于语句setInterval(func, 100, args),js引擎每隔100毫秒就会把func添加到任务队列一次。

相同点:

  1. 两者都会加入同一个队列,等待线程空闲时执行。
  2. 两者都无法保证在何时执行回调,因为无法知道线程何时空闲。

不同点

  1. setTimeout只会将函数添加到任务队列一次,而setInterval则是循环往队列中添加函数。
  2. setTimeout可以保证函数在指定的时间间隔内不会执行,而setInterval无法保证(有可能出现接近连续执行的情况,后面会分析原因)。

运行机制

setTimeout

setTimeout的运行机制相对简单,即在执行该语句时,设置一个定时器,定时时间置为所设置的延时,当计时结束后,将传入的函数加入任务队列,之后的执行就交给任务队列负责。

setTimeout函数本身会返回一个句柄,我们可以在函数执行前通过向clearTimeout传入该句柄取消函数的执行。示例代码如下:

function func(message){
	;
}
//设置100毫秒后执行func函数
var timer = setTimeout(func, 100, "你好");

function cancel(){
	clearTimeout(timer);   //取消超时调用
}

上述代码将在100毫秒后执行func函数,弹出一个内容为"你好"的对话框。如果在100毫秒内调用了cancel,就可以取消func函数的执行。

setInterval

setInterval本质上就是每隔一定的时间向任务队列添加回调函数。但setInterval有一个原则:在向队列中添加回调函数时,如果队列中存在之前由其添加的回调函数,就放弃本次添加(不会影响之后的计时)。另外也可以通过clearInterval方法移除定时器,使用方法同clearTimeout。

由于setInterval只负责定时向队列中添加函数,而不考虑函数的执行,那么我们考虑一下下面的情况:

假设线程执行完setInterval(func, 100, args)后处于完全空闲状态(即只要向任务队列添加函数就会立即执行)。而func是一个相对复杂的函数,执行该函数需要90毫秒。那么函数的执行过程就会变成下图所示:

1684999072_646f0ba01a6d0ee8c7c99.png!small?1684999072476

从图中可以看到,从上次函数执行完毕,到下次开始执行,之间只间隔了10毫秒,而不是我们所希望的每隔100毫秒执行一次(因为setInterval只关注任务添加,不关注任务执行)。

由于上述机制,在很多情况下,setInterval都会遇到一些性能问题。就拿上面的例子来说,我们的本意可能是每隔100毫秒执行一次函数,结果只等待了10毫秒就又执行了一次。另外,对于复杂的实际情况,setInterval经常出现两次的执行间隔相差甚远的情况,对于用户能感知到的操作,这会带来很不好的用户体验。因此在实际编码中,开发者通常会使用setTimeout来模拟实现setInterval效果(下面会有举例)。

而如果线程一开始是繁忙的,直到150毫秒处才进入空闲状态(假设func执行时长为10毫秒),那么实际的运行将变成下图所示:

1684998891_646f0aebc0493e9896b28.png!small?1684998892247

这里在100毫秒处向队列添加func时,由于线程繁忙,上次添加的func还在队列中等待,因此直接丢弃本次要添加的函数,但在200毫秒时仍然重新向队列中添加func。

应用场景

setTimeout

setTimeout主要用于需要进行延时调用的场景中。此外,由于setInterval存在的性能问题,在实际的编码中,开发人员通常会使用setTimeout来模拟setInterval,以防止出现函数连续执行的情况。如对于下面的代码:

function func(args){
  //函数本身的逻辑
  ...
}
var timer = setInterval(func, 100, args);

我们可以通过以下代码来实现:

var timer;
function func(args){
  //函数本身的逻辑
  ...
  //函数执行完后,重置定时器
  timer = setTimeout(func, 100, args);
}
timer = setTimeout(func, 100, args);

利用setTimeout保证在指定的时间内不会执行的特点,我们可以在执行完上次的回调函数后,重置定时器,实现循环执行func的效果,并且从上次执行完毕到下次执行开始,至少会经过100毫秒。这在实际的编码中通常会带来较大的性能提升,同时函数的执行间隔也会相对稳定。

setInterval

尽管存在上述性能问题,setInterval的使用场景相对较少,但当所使用的接口来自外部(即回调函数本身无法修改)时,就必须通过setInterval来实现循环执行了。此外,对于动画效果来说,我们通常会希望动画运行的更加平滑(也就是希望函数运行得更频繁),这时使用setInterval往往更加流畅。

除了这类情况,开发者一般不会使用setInterval方法进行循环调用。

补充说明

setTimeout与setInterval的第一个参数可以是一个匿名函数,也可以是一个函数名,或者是一个字符串,如下面的写法都是合法的:

function func(msg){
  ...
}
//传入回调函数名
setTimeout(func, 100, "夕山雨");
//传入匿名函数
setTimeout(function(name){
  ...
}, 100, "夕山雨");
//传入字符串,js引擎会将其解析为函数体
setTimeout("", 100);

但是传入如下的格式就可能报错:

setTimeout(func("夕山雨"), 100);

因为这种写法实际上是先调用func函数,然后再将返回值添加到任务队列。如果func的返回值不是函数(或可执行的字符串),那么程序就会报错;如果返回值是函数,则会将返回的函数添加到任务队列。该情况可以写成下面的形式:

//将其作为字符串传入,就可以被正确解析
setTimeout("func('夕山雨')", 100);

此外,当给setTimeout传入的延迟时间为0时,并不代表回调函数会立即执行。实际上浏览器规定的有一个默认的最短计时时间,对于现代浏览器,这个时间一般为4毫秒(老版本的浏览器则会更长一些)。也就是说,即使传入的延迟时间为0,浏览器也会至少在4毫秒后才会执行。

上述补充说明同样适用于setInterval。

总结

setTimeout与setInterval都是通过一个定时器控制回调函数的执行,但由于javascript单线程的特点,两者都不能准确控制函数的执行时间点,这点还请开发者注意。如果函数只需要执行一次,很显然我们会使用setTimeout来实现;如果是循环执行的情况,如果我们希望函数执行频率不那么高,并且间隔更稳定,通常是使用setTimeout模拟实现setInterval效果。

总的来说,虽然都被用于函数延迟执行,但两者的运行机制有本质上的区别,所以在使用的时候请注意区分。

# 系统安全 # 数据安全 # javascript # 前端开发
本文为 FreeBuf-357997 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
FreeBuf-357997 LV.6
专注技术的程序员一枚
  • 51 文章数
  • 3 关注者
网红直播带货你了解多少
2023-06-19
redis探秘:选择合适的数据结构,减少80%的内存占用,这些点你get到了吗?
2023-05-30
ElasticSearchRepository和ElasticSearchTemplate的使用
2023-05-26
文章目录