js基础之setTimeout与setInterval原理分析

js基础之setTimeout与setInterval原理分析

文章图片

setTimeout与setInterval概述setTimeout与setInterval是JavaScript引擎提供的两个定时器方法 , 分别用于函数的延时执行和循环调用 。 前者的主要思想是通过一个定时器 , 让函数在计时结束后再执行;后者则是每隔一定的时间 , 就启动一次函数的执行 。
从原理来看 , 两者似乎并不复杂 。 但由于JavaScript引擎是单线程的 , 这就让上述两个定时器的实际执行变得稍微复杂了一些 。 下面我们来看一下两者的运行机制与需要注意的问题 。
基本原理知识铺垫单线程模型:由于JavaScript被设计为用在浏览器环境 , 而该环境下存在大量可能发生冲突的DOM操作 , 为了避免进行复杂的冲突处理(可能存在的冲突数量几乎不可预测) , JavaScript的设计者舍弃了java的多线程模型(该模型下 , 执行引擎同时可以做几件事 , 但要进行线程同步) , 将其设计成了一门单线程语言(执行引擎在同一时间只做一件事) 。
注意:这里的单线程是指JavaScript的主线程只有一个 。 除了这个主线程 , JavaScript还有一个I/O线程 , 通过事件循环来处理I/O问题 , 但两者之间相对独立 , 不需要进行状态同步 , 因此我们仍然可以把JavaScript看成一门单线程语言 。
任务队列:所谓任务队列 , 就是用于存储等待执行的任务的队列 。 由于JavaScript是一门单线程语言 , 如果当前有一个任务需要执行 , 但JavaScript引擎正在执行其他任务 , 那么这个任务就需要放进一个队列中进行等待 。 等到线程空闲时 , 就可以从这个队列中取出最早加入的任务进行执行(类似于我们去银行排队办理业务 。 单线程相当于说这家银行只有一个服务窗口 , 一次只能为一个人服务 , 后面到的就需要排队 , 而任务队列就是排队区 , 先到的就优先服务) 。
注意:如果当前线程空闲 , 并且队列为空 , 那每次加入队列的函数将立即执行 。
setTimeout与setIntervalsetTimeout(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无法保证(有可能出现接近连续执行的情况 , 后面会分析原因) 。
运行机制setTimeoutsetTimeout的运行机制相对简单 , 即在执行该语句时 , 设置一个定时器 , 定时时间置为所设置的延时 , 当计时结束后 , 将传入的函数加入任务队列 , 之后的执行就交给任务队列负责 。
setTimeout函数本身会返回一个句柄 , 我们可以在函数执行前通过向clearTimeout传入该句柄取消函数的执行 。 示例代码如下:
function func(message){\t;//设置100毫秒后执行func函数var timer = setTimeout(func 100 \"你好\");function cancel(){\tclearTimeout(timer);//取消超时调用上述代码将在100毫秒后执行func函数 , 弹出一个内容为\"你好\"的对话框 。 如果在100毫秒内调用了cancel , 就可以取消func函数的执行 。
setIntervalsetInterval本质上就是每隔一定的时间向任务队列添加回调函数 。 但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 。
应用场景setTimeoutsetTimeout主要用于需要进行延时调用的场景中 。 如之前一篇文章介绍的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效果 。
【js基础之setTimeout与setInterval原理分析】总的来说 , 虽然都被用于函数延迟执行 , 但两者的运行机制有本质上的区别 , 所以在使用的时候请注意区分 。

    推荐阅读