在开发中,遇到了一种需求,有个接种情况,一支疫苗是多人份的,如果打开了,那么必须在有效期内注射完,否则这个疫苗就应该废弃,所以当这支疫苗打开的时候,那么就需要从有效期的最大时间开始定时,一直到有效期的时间变为0,此时,如果还没有打完,那么这个疫苗就要废弃。
当然我们需要在画面上显示这个疫苗什么时间被打开,以及什么时候到期,这样医生就不用自己看时间,直接从画面就可以看到这个疫苗什么时间废弃,这样对于客户来说减轻负担和工作量,所以我们需要对画面上一览查询的多人份疫苗需要定时的刷新,比如10s一刷新,直到他的有效期变为0,同时可能还会有其他种类的多人份疫苗,所以我们需要定时刷新数据。
第一种想法:在后端使用定时任务,固定时间间隔来加载数据并显示。
第二种想法:在前端进行定时轮询,这样只有在该画面打开的时候在会从后台加载数据,不会在项目刚启动就从后台加载数据。
所以,我选择了第二种做法。选择了setInterval定时函数来进行操作。下面我会具体分享一下Scheduling中的setTimeout和setInterval函数
使用场景:在我们不需要立即执行这个函数,需要等待一段时间在执行的时候,我们把这种函数就叫做 定时函数。
分类:
setTimeout
: 这个函数允许在等待一段时间后再执行(只执行一次);setInterval
: 这个函数允许在固定的时间间隔后规律的反复执行。
这两个函数并不是JavaScript规范中的一部分,但是大多数环境都有定时器和提供的方法,尤其是,他们支持所有的浏览器并且支持NodeJs.
setTimeout
函数
语法:let timerId = setTimeout(func|code, delay[, arg1, arg2...])
参数解释:
fun|code
:要定时执行的字符串或者函数。通常的话是一个函数,对于一些历史原因,字符串也可以被传递,但是一般不会推荐使用字符串。
delay
: 延时,表示要延时多久以后该函数才被执行,参数值应该是毫秒(1s=1000ms)
arg1,arg2
:函数的参数(这个是不支持IE9及以下版本)
看下面一个例子,这个例子是说,在1s后,会调用sayHi
函数,并且执行alert
,代码示例如下:
function sayHi() {
alert('Hello');
}
setTimeout(sayHi, 1000);
带参数的示例如下:
function sayHi(phrase, who) {
alert( phrase + ', ' + who );
}
setTimeout(sayHi, 1000, "Hello", "John");
如果第一个参数是字符串,JavaScript会通过它创建一个函数,所以下面的也会被执行:
setTimeout("alert('Hello')", 1000);
但是我们并不建议采用字符串的形式,所以下面这样,使用函数来替代字符串:
setTimeout(() => alert('Hello'), 1000);
注意下面的特殊例子,只可以传函数名字,不要传函数名字加(),否则是不可以运行的。
// wrong!
setTimeout(sayHi(), 1000);
上述代码异常,因为setTimeout
函数希望得到的是一个函数引用,sayHi()
函数运行后,返回它的运行结果传递给setTimeout
。在我们的例子中sayHi()
的结果是undefined
的。所以不会有任何的东西被定时执行。
clearTimeout
调用setTimeout
会返回一个time identifier
timerId
,我们可以使用timerId
来取消执行
取消执行的语法如下:
let timerId = setTimeout(...);
clearTimeout(timerId);
下面这个例子中,我们定时调用函数,但是我们改变了想法不想再调用,结果就是什么都不会发生:
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // timer identifier
clearTimeout(timerId);
alert(timerId); // same identifier (doesn't become null after canceling)
你可以复制上面这段代码去运行,会发现,输出的是一个数值,我实验输出的是11,浏览器是chrome。NodeJs返回一个事件对象。
setInterval
语法:let timerId = setInterval(func|code, delay[, arg1, arg2...])
setInterval
函数的参数含义和setTimeout
的含义相同,不同的是,setInterval
是按照固定的时间间隔定时执行里面的函数,不是只执行一次。
如果想停止,我们可以调勇这个方法clearInterval(timerId)
下面是该方法的示例代码:
// 间隔两秒执行一次
let timerId = setInterval(() => alert('tick'), 2000);
// 5s后停止执行
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
当展示alert/confirm/prompt
的时候,在浏览器IE和Firefox中内部计时器会继续执行;但是在chrome和Opera和Safari的的内部计时器就会停止。
所以如果你在以上执行代码,并且没有关闭alert窗口。接着在Firefox或者IE中,alert窗口会接着显示(2s过后)但是在 Chrome/Opera/Safari中可能会等待更多的时间。
递归setTimeout
有两种形式可以规律性的执行一些代码。
一个是使用setInterval
。另外一种形式就是递归setTimeout
。
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
在(*)这个结束的时候会开始调用下一个。
递归setTimeout
比使用setInterval
更加灵活,因为使用前者下次的定时时间可以与上次的不相同,取决于当前调用的结果。
例如,我们需要写一个服务,这个服务每隔5s发送一个请求给服务器请求数据,但是为了防止服务过载,他应该增加时间间隔,比如10s,20s,30s,40s…
let delay = 5000;
let timerId = setTimeout(function request() {
if (request failed due to server overload) {
// 下次执行的时候增加时间间隔
delay *= 2;
}
timerId = setTimeout(request, delay);
}, delay);
如果我们经常有CPU需求,那么我们可以测量执行所花费的时间,并计划下一次调用。
递归调用setTimeout
可以保证执行期间有延时,但是setInterval
是不可以的。
来看下面两个比较:
setInterval
let i = 1;
setInterval(function() {
func(i);
}, 100);
递归setTimeout
let i = 1;
setTimeout(function run() {
func(i);
setTimeout(run, 100);
}, 100);
对于setInterval
,内部定时器会每隔100ms执行一次func(i)
.
真正的延时在setInterval
中调用func
的时间是少于在代码中给定的时间的。那是正常的,因为func
的执行也会消耗一部分时间。很可能func
的执行结果证明比我们所期待的时间更多,时间会大于100ms。在这种情况下,引擎会等待func
完成,然后检查调度程序以及时间是否结束,如果结束就再次立即执行。另外一种情况就是,函数总是执行比delay
设定的时间更长,结果就是中间都没有停止就会继续执行下一次调用。下面是一个递归调用setTimeout
的图片解释:
递归setTimeout
可以保证固定的时间间隔。(这里是100ms)
那是因为一个新的调用计划在前一个的末尾等待。
垃圾收集
当一个函数被传递给setInterval
或者setTimeout
的时候,一个内部的引用就会被创建并且保存在定时器中。它可以保护该函数防止被当做垃圾进行收集,即使这里没有其他引用到它。
对于setInterval
函数来说,它会一直待在内存中直到clearInterval
方法被调用。
这里也存在一个负面影响,一个函数的引用的外部变量的环境中时,当它存活的时候,外部变量也同样存活。它们或许比函数本身占用了更多的内存。所以当我们不需要定时函数的时候,最好的办法是停止它,即使定时函数很小。
setTimeout(…,0)
这是一个特殊的使用方法:setTimeout(func,0)
这个定时是尽可能快的执行,因为定时器在当前的代码执行完以后就会调用它。换句话说就是异步执行。
例如,下面输出是Hello
,然后立即输出World
:
setTimeout(() => alert("World"), 0);
alert("Hello");
为什么是这样?因为第一行“在0ms后调用会放在日历中”。 但是调度程序只会在当前代码完成后“检查日历”,因此“Hello”是第一个,而“World”是在它之后。
拆分CPU饥饿任务(splitting cpu-hungry tasks)
对于CPU饥饿任务,我们有一个技巧,可以使用setTimeout
来解决。
例如,语法高亮脚本(用于对此页面上的代码示例进行着色)非常耗费CPU。对于高亮代码,它扮演这分析,创建很多彩色元素,并把他们添加到文档中,这会消耗很多。甚至会导致浏览器奔溃。
所以我们可以将这个又长又大的文本碎片化,第一次处理100行,然后计划处理下100行使用setTimeout(...,0)
,诸如此类。
为了清楚起见,让我们对我们的考虑做一个简单的例子。我们有个函数,作用是从1计数到1000000000.
如果你运行它,CPU将会挂起。对于明显引人注目的服务器端JS,如果您在浏览器中运行它,然后尝试单击页面上的其他按钮 - 您将看到整个JavaScript实际上已暂停,在完成之前对于任何其他操作都是无效的。
let i = 0;
let start = Date.now();
function count() {
// do a heavy job
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
浏览器或许会给出提示,说是脚本运行时间太长的警告。(希望不要出来,因为数字并不是很大)。下面使用setTimeout
来解决:
let i = 0;
let start = Date.now();
function count() {
// do a piece of the heavy job (*)
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count, 0); // schedule the new call (**)
}
}
count();
现在浏览器在计数期间正常运行。
我们在(*)做了下面这个工作:
首先执行了i从1到1000000,然后又执行1000001到2000000,以此类推,用while
来判断i是否可以被1000000相除。
如果我们到现在还没有做,那么下次调用会在(**)处进行。
计数执行之间的暂停为JavaScript引擎提供足够的“呼吸”以执行其他操作,以对其他用户操作做出反应。
值得注意的是,两种变体 - 无论是否通过setTimeout分割作业 - 都具有可比性。 总计数时间没有太大差异。
为了让它们更接近,让我们改进一下。
我们将在count()的开头调用:
let i = 0;
let start = Date.now();
function count() {
// move the scheduling at the beginning
if (i < 1e9 - 1e6) {
setTimeout(count, 0); // schedule the new call
}
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
}
}
count();
现在,当我们开始count()并知道我们需要更多地count()时,我们会在完成工作之前立即安排。
如果你运行它,很容易注意到它花费的时间要少得多。
嵌入式计时器在浏览器中的延迟最小
在浏览器中,嵌套计时器运行的频率存在限制。 HTML5标准说:“在五个嵌套定时器之后,间隔被强制为至少4毫秒
让我们通过下面的例子演示它的含义。 其中的setTimeout
调用在0ms后重新调度自身。在次数数组中每次都会从前一个调用的真实时间。 真正的延迟是什么样的? 让我们来看看:
let start = Date.now();
let times = [];
setTimeout(function run() {
times.push(Date.now() - start); // 记住前一次调用的时间延时。
if (start + 100 < Date.now()) alert(times); // 在100ms后显示延时
else setTimeout(run, 0); // else 重新调用
}, 0);
// 这个例子的输出结果如下
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
可以从结果看出,起初是立即执行,但是后来慢慢的形成了时间间隔9,15,20,24.。。。
这个限制就来自于很久之前,大多脚本都依赖于它,所以这也是它存在的历史原因。
对于服务端的JavaScript来说,这种限制是不存在的,这里存在其他的方式来立即异步调用任务,比如在NodeJS中可以用process.nextTick
和setImmediate
。所以这个概念只是针对浏览器的。
允许浏览器呈现
浏览器里面的脚本的另一个好处就是可以展示一个进度条或者其它的东西给用户。那是因为在脚本完成之后,浏览器通常会全部重新绘制画面 。
因此,如果我们执行单个巨大的功能,那么即使它发生了变化,更改也不会反映在文档中,直到完成为止
<div id="progress"></div>
<script>
let i = 0;
function count() {
for (let j = 0; j < 1e6; j++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>
当你运行这段代码的时候,对于i值的改变它将在整个count函数执行完之后才会显示出来。有个坏处就是画面必须一直等到count执行完以后才可以点击做其他的操作,下面使用setTimeout
的话,就不存在这个问题,但是画面上的i值一直会是变化的。
如果我们使用setTimeout将其拆分为多个部分,则会在运行之间应用更改,因此这看起来更好。
总结:
setInterval(func,delay,...args)
和setTimeout(func,delay,...args)
都可以执行定时,区别在于前者可以规律的进行执行,后者只可以执行一次。- 如果定时器不用的话,我们应该对其进行取消,使用
clearInterval
或者clearTimeout
,原因上面提到过,引用外部的变量会随着定时器一直存活下去。 - 递归调用
setTimeout
比setInterval
更加灵活。他们可以保证两次执行间的最小的时间。 setTimeout(...,0)
的使用。
参考文章:https://javascript.info/settimeout-setinterval#recursive-settimeout