Javascript 调度: setTimeout and setInterval
我们可能决定不立刻执行一个函数,而是某时间之后执行,一般我们称为“调度执行”。
有两种方法可以实现:
setTimeOut
指某时间间隔之后执行一次函数。setInterval
指安装一定时间间隔有规律执行函数。
这些函数不是Javascript规范的一部分,但是大多数环境有内部调度并提供了这些方法。特别是,所有浏览器和Node.Js都支持。
setTimeout
语法:
let timerId = setTimeout(func|code, delay[, arg1, arg2...])
参数
func|code
被执行的函数或代码字符串,通常是函数,因为历史原因,代码字符串被允许,但不建议使用。
delay
运行之前的时间数,毫秒为单位。
arg1,arg2...
被执行函数的参数(IE9以下不支持)
举例,下面代码1秒之后执行sayHi()
function sayHi() {
alert('Hello');
}
setTimeout(sayHi, 1000);
带参数情况:
function sayHi(phrase, who) {
alert( phrase + ', ' + who );
}
setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John
如果第一个参数是字符串,那么Javascript为之创建一个函数,所以,下面代码也正常运行:
setTimeout("alert('Hello')", 1000);
但不建议使用字符串,使用函数代替,如下:
setTimeout(() => alert('Hello'), 1000);
传递函数,但不运行
注意,开发者有时错误地在函数名称后面增加括号:
// wrong!
setTimeout(sayHi(), 1000);
这个代码不能正常执行,因为setTimeout
希望参数为函数引用,这里是sayHi()
是函数的执行返回值,即表达式的执行结果传递给函数。这里sayHi()
返回值为undefined
(函数返回值为空),所以没有啥需要调度。
使用clearTimeout取消调度
调用setTimeout
返回一个“timer标识符”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)
我们能看到alter
输出,在浏览器中时间标识符为数字,其他环境可能为其他值,如NodeJS中返回时间对象,并带有额外的方法。
因为没有统一的规范,所以也没有问题。
对浏览器,timer在html5中描述了规范。
setInterval
方法setInterval
语法与setTimeout
一致:
let timerId = setInterval(func|code, delay[, arg1, arg2...])
所有参数意思相同,但与setTimeout
不同的是,其运行函数不只一次,而是有规律地根据某个时间间隔运行。
为了停止运行,可以调用clearInterval(timerId)
。
下面示例每个2秒显示消息,5秒过后,输出停止:
// repeat with the interval of 2 seconds
let timerId = setInterval(() => alert('tick'), 2000);
// after 5 seconds stop
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
Chrome/Opera/Safari中模态窗口冻结时间
在ie或firefox浏览器中,当显示alert/confirm/prompt
时,内部时钟继续计时,但是在Chrome/Opera/Safari
中内部时钟却冻结。
所以如果你运行上面代码,不关闭alter
窗口一段时间,那么Firefox/IE 下面一个alert
将立即显示(因为前面的执行也在计时),而Chrome/Opera/Safari
则超过2秒才执行后面代码(因为alert出现时,内部计时冻结)。
递归setTimeout
两种方式可以定期运行函数,一种是setInterval
,另一种是递归执行setTimeout
,如下:
/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
上面代码中setTimeout
调度下一次调用正好是当前调用的最后一行(*号行)。
递归setTimeout
方式比setInterval
更灵活,因为其可以根据当前执行结果,让下次有不同的调度。
举例,我们需要写一个服务,没5秒发送一次请求至服务器请求数据,但如果服务器超负荷,应该增加间隔时间至10,20,40秒…,看下面伪代码:
let delay = 5000;
let timerId = setTimeout(function request() {
...send request...
if (request failed due to server overload) {
// increase the interval to the next run
delay *= 2;
}
timerId = setTimeout(tick, 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)一次:
你注意到了吗?
对Interval方式的func函数调度实际间隔时间少于100毫秒!
显然,因为执行func本身需要消费一部分间隔时间。可能func执行时间长于我们预期时间,甚至超过100号秒。
如果不超过100毫秒,引擎等待func执行完成,然后检查调度,如果时间到了,立刻再次运行。另一种情况,函数执行总是超过间隔时间,那么函数将根本没有暂停立刻执行。
下图是递归setTimeout方式运行图:
递归setTimeout方式确保固定时间间隔
因为新的调用被计划在前一个执行之后。
垃圾回收
当函数被传递给setInterval/setTimeout
,有一个内部引用被创建指向它并保留在调度中,其阻止函数被垃圾回收,即使没有其他引用指向它。
// the function stays in memory until the scheduler calls it
setTimeout(function() {...}, 100);
对于setInterval,函数一直驻留内存,直到cancelInterval被调用。这样有些副作用,函数引用外部词法环境,所以一直激活,外部变量也是。因此需要比函数自身消耗的内存更多。所以,当我们不需要调度函数时,最好取消它,即使其很小。
setTimeout(…,0)
有个特别的用途:setTimeout(func,0)
,调度函数func尽可能快地执行,但调度将在当前代码完成之后执行。
所以函数调度正是运行在当前代码之后,也就是说,异步执行,下面示例先输出“hello”,然后立刻输出“World”。
setTimeout(() => alert("World"), 0);
alert("Hello");
第一行代码设置调用为0ms之后,但是调度仅在当前代码完成之后开始检查时钟,所以“hello”先输出,然后是“world”。
分割耗CPU任务
巧妙使用setTimeout可以分割耗CPU任务。举例,语法高亮脚本(通常给某页一些示例代码颜色化)是相对耗CPU的任务。为了高亮代码,需要执行分析,创建很多演示元素,并增加他们至页面文档中,对大量文本来说,需要很长时间,这可能引起浏览器挂起,用户无法接受。
所以我们能分割长文本为多个块,首先第一个100行,然后使用setTimeout(...,0)
计划另一个100行,一直往复。
为了描述清除,我们举个简单的示例,有个函数需要计数,从1到1000000000。
如果你直接运行,cpu将挂起,对服务器段脚本也是很明显,如果你运行在浏览器段,试图点击页面上的其他按钮,会发现整个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();
现在浏览器ui在“count”执行过程中功能正常。我们主要做了几部分工作:
- 首先运行 : 1…1000000.
- 第二次运行: 1000001…2000000.
- …往复运行,如果while检查i正好能被1000000整除。
如果还没有完成,下一个调用被调度(*号行)
count之间的暂停执行让Javascript引擎有了喘息时间,正好可以执行其他任务,用于响应用户动作。
值得注意的是:两种方式:采用和不采用setInterval分割任务,在速度上有比较,整个计数时间没有差异。
为了说明更清楚,让我们改进示例,我们把调度代码移动到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标准描述:5个嵌套时钟之后。。。间隔被迫需要4毫秒。
下面示例演示说明,setTimeout被调度0ms后开始运行,每次调用记录实际运行时间,我们看看延迟情况:
let start = Date.now();
let times = [];
setTimeout(function run() {
times.push(Date.now() - start); // remember delay from the previous call
if (start + 100 < Date.now()) alert(times); // show the delays after 100ms
else setTimeout(run, 0); // else re-schedule
}, 0);
// an example of the output:
// 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,没有这样的限制,也有其他方式调度立刻执行的异步任务,如process.nextTick 和 setImmediate 在 Node.js中。因此只有浏览器才有这样概念。
浏览器渲染过程
因为浏览器通常脚本执行完成后,全部渲染给用户,利用延迟技术可以给用户显示精度条或其他方式的过程。
我们有一个巨大的函数,其改变某对象,改变直到执行完成才反应出来。示例如下:
<div id="progress"></div>
<script>
let i = 0;
function count() {
for(let j = 0; j < 1e6; j++) {
i++;
// put the current i into the <div>
// (we'll talk more about innerHTML in the specific chapter, should be obvious here)
progress.innerHTML = i;
}
}
count();
</script>
如果你运行代码,i的值是在整个代码执行完成后才显示。
如果我们使用setTimeout去分割代码,修改值被分配至每个片段中,效果更好:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// do a piece of the heavy job (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e9) {
setTimeout(count, 0);
}
}
count();
</script>
现在div中的值不断在增长。
总结
- 两个方法
setInterval(func, delay, ...args)
和setTimeout(func, delay, ...args)
都可以实现定时运行函数。 - 取消调度执行,可以调用
clearInterval/clearTimeout
,使用setInterval/setTimeout
函数的返回值. - 嵌套
setTimeout
调用比setInterval
更灵活. 也能保证执行之间的最小时间(异步方式). - 零时间调度
setTimeout(...,0)
用于调度执行 “尽可能短, 但必须在当前代码执行完成之后”.
使用 setTimeout(...,0)
的场景有:
- 分割耗时CPU任务至多个片段,避免执行代码时程序挂起。
- 为在执行任务的同时,让浏览器可以执行其他任务(绘制进度条).
注意,所有调度方法不能保证精确的延迟时间,我们不能依赖调度代码实现。
举例,浏览器内置时钟慢可能有许多原因:
- CPU超负荷.
- 浏览器 tab 是后台模式.
- 笔记本电脑电池.
所有可能降低最小计时器方案(最小延迟)300毫秒甚至1000 ms取决于浏览器和设置。
本文参考链接:https://blog.csdn.net/neweastsun/article/details/73843584