写点什么

js 基础之 setTimeout 与 setInterval 原理分析

  • 2024-09-19
    北京
  • 本文字数:3255 字

    阅读完需:约 11 分钟

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 毫秒。那么函数的执行过程就会变成下图所示:



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


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


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



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

应用场景

setTimeout

setTimeout 主要用于需要进行延时调用的场景中。如之前一篇文章介绍的js基础之函数的节流与防抖,就是 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 往往更加流畅,具体请参考之前的文章使用原生js实现简单动画效果


除了这类情况,开发者一般不会使用 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 效果。


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

发布于: 刚刚阅读数: 7
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
js基础之setTimeout与setInterval原理分析_京东科技开发者_InfoQ写作社区