定时器和计时器
调度:setTimeout和setInterval
我们可能决定不立即执行某个功能,但在某个时间之后执行。这叫做“安排一个电话”。
它有两种方法:
setTimeout
允许我们在一段时间后运行一次函数。setInterval
允许我们重复运行一个函数,从时间间隔开始,然后以该间隔连续重复。
这些方法不是JavaScript规范的一部分。但是大多数环境都有内部调度程序并提供这些方法。特别是,它们在所有浏览器和Node.js中都受支持。
的setTimeout
语法:
let
timerId =
setTimeout
(
func|
code,
[
delay]
,
[
arg1]
,
[
arg2]
,
...
)
参数:
func|code
- 函数或要执行的代码串。通常,这是一个功能。由于历史原因,可以传递一串代码,但不建议这样做。
delay
- 运行前的延迟,以毫秒(1000毫秒= 1秒)为单位,默认为0。
arg1
,arg2
...- 该函数的参数(IE9中不支持)
例如,此代码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()
运行函数,并将其执行结果传递给setTimeout
。在我们的例子中,结果sayHi()
是undefined
(函数不返回任何内容),因此没有安排任何内容。
使用clearTimeout取消
调用setTimeout
返回“计时器标识符” 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)
正如我们从alert
输出中看到的,在浏览器中,计时器标识符是一个数字。在其他环境中,这可能是其他原因。例如,Node.js返回带有其他方法的计时器对象。
同样,这些方法没有通用规范,所以没关系。
对于浏览器,定时器在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
)
;
alert
显示在大多数浏览器中,包括Chrome和Firefox,内部计时器会在显示时继续“滴答” alert/confirm/prompt
。
因此,如果您运行上面的代码并且暂时不关闭alert
窗口,那么下一步alert
将立即显示在您执行此操作时。警报之间的实际间隔将短于2秒。
嵌套的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
(
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
内部调度程序将func(i)
每100ms 运行一次:
你注意到了吗?
func
呼叫之间的实际延迟setInterval
小于代码!
这是正常的,因为func
执行所花费的时间“消耗”了间隔的一部分。
这可能是func
的执行结果是比我们预期的时间更长,花的时间超过100毫秒。
在这种情况下,引擎等待func
完成,然后检查调度程序,如果时间到了,立即再次运行它。
在边缘情况下,如果函数总是执行的时间长于delay
ms,则调用将完全没有暂停。
这是嵌套的图片setTimeout
:
嵌套setTimeout
保证固定延迟(此处为100ms)。
那是因为计划在前一个呼叫结束时进行新呼叫。
传入函数时setInterval/setTimeout
,会为其创建内部引用并将其保存在调度程序中。即使没有其他引用,它也会阻止函数被垃圾回收。
// the function stays in memory until the scheduler calls it
setTimeout
(
function
(
)
{
...
}
,
100
)
;
对于setInterval
函数保留在内存中直到clearInterval
被调用。
有副作用。函数引用外部词汇环境,因此,在它存在的同时,外部变量也存在。它们可能比函数本身占用更多的内存。因此,当我们不再需要预定的功能时,最好取消它,即使它非常小。
零延迟setTimeout
有一个特殊的用例:setTimeout(func, 0)
或者只是setTimeout(func)
。
这样func
可以尽快安排执行。但是调度程序只有在当前执行的脚本完成后才会调用它。
因此该函数被安排在“当前脚本之后”运行。
例如,输出“Hello”,然后立即“World”:
setTimeout
(
(
)
=>
alert
(
"World"
)
)
;
alert
(
"Hello"
)
;
第一行“在0ms之后将呼叫置于日历中”。但是调度程序只会在当前脚本完成后“检查日历”,因此"Hello"
首先,然后"World"
- 在它之后。
还有与延迟浏览器相关的零延迟超时用例,我们将在事件循环一章中讨论:微任务和宏任务。
在浏览器中,嵌套计时器运行的频率存在限制。的HTML5标准表示:“后5个嵌套计时器,间隔被强制为至少4毫秒。”。
让我们通过下面的例子演示它的含义。其中的setTimeout
调用以零延迟重新调度自身。每次调用都会记住times
数组中前一次的实时。真正的延迟是什么样的?让我们来看看:
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)
;
// else re-schedule
}
)
;
// 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...
。调用之间的4毫秒强制性延迟发挥作用。
类似的事情发生在我们使用setInterval
而不是setTimeout
:零延迟setInterval(f)
运行f
几次,然后延迟4+ ms。
这种限制来自古代,许多脚本依赖它,因此它存在是出于历史原因。
对于服务器端JavaScript,该限制不存在,并且存在其他方法来调度立即异步作业,例如setImmediate for Node.js. 所以这个注释是特定于浏览器的。
摘要
- 方法
setTimeout(func, delay, ...args)
并setInterval(func, delay, ...args)
允许我们func
在delay
毫秒之后运行一次/定期运行。 - 要取消执行,我们应该
clearTimeout/clearInterval
使用返回的值调用setTimeout/setInterval
。 - 嵌套
setTimeout
调用是一种更灵活的替代方法setInterval
,允许我们更精确地设置执行之间的时间。 - 使用
setTimeout(func, 0)
(相同setTimeout(func)
)的零延迟调度用于“尽快,但在当前脚本完成后”调度呼叫。 - 浏览器将五个或更多嵌套调用的最小延迟限制
setTimeout
为setInterval
(在第五次调用之后)到4ms。这是出于历史原因。
请注意,所有调度方法都不能保证确切的延迟。
例如,浏览器中的计时器可能会因为很多原因而变慢:
- CPU过载。
- 浏览器选项卡处于后台模式。
- 笔记本电脑是电池。
所有这些都可能将最小定时器分辨率(最小延迟)提高到300ms甚至1000ms,具体取决于浏览器和操作系统级别的性能设置。
任务
编写一个printNumbers(from, to)
每秒输出一个数字的函数,从开始到from
结束to
。
制作解决方案的两个变体。
- 用
setInterval
。 - 使用嵌套
setTimeout
。
使用setInterval
:
function
printNumbers
(
from
,
to
)
{
let
current =
from
;
let
timerId =
setInterval
(
function
(
)
{
alert
(
current)
;
if
(
current ==
to)
{
clearInterval
(
timerId)
;
}
current++
;
}
,
1000
)
;
}
// usage:
printNumbers
(
5
,
10
)
;
使用嵌套setTimeout
:
function
printNumbers
(
from
,
to
)
{
let
current =
from
;
setTimeout
(
function
go
(
)
{
alert
(
current)
;
if
(
current <
to)
{
setTimeout
(
go,
1000
)
;
}
current++
;
}
,
1000
)
;
}
// usage:
printNumbers
(
5
,
10
)
;
请注意,在两种解决方案中,在第一次输出之前都存在初始延迟。1000ms
第一次调用该函数。
如果我们还希望函数立即运行,那么我们可以在单独的行上添加一个额外的调用,如下所示:
function
printNumbers
(
from
,
to)
{
let
current=
from
;
function
go
(
)
{
alert
(
current)
;
if
(
current==
to)
{
clearInterval
(
timerId)
;
}
current++
;
}
go
(
)
;
let
timerId=
setInterval
(
go,
1000
)
;
}
printNumbers
(
5
,
10
)
;
在下面的代码中有一个setTimeout
调度计划,然后运行繁重的计算,完成时间超过100毫秒。
计划的功能何时运行?
- 循环之后。
- 在循环之前。
- 在循环的开始。
什么alert
会显示?
let
i =
0
;
setTimeout
(
(
)
=>
alert
(
i)
,
100
)
;
// ?
// assume that the time to execute this function is >100ms
for
(
let
j =
0
;
j <
100000000
;
j++
)
{
i++
;
}
任何setTimeout
只有在当前代码完成后才会运行。
该i
会是最后一个:100000000
。
let
i =
0
;
setTimeout
(
(
)
=>
alert
(
i)
,
100
)
;
// 100000000
// assume that the time to execute this function is >100ms
for
(
let
j =
0
;
j <
100000000
;
j++
)
{
i++
;
}
本文仅提供参考,是本人闲时所写笔记,如有错误,还请赐教,作者:阿蒙不萌,大家可以随意转载