调度:setTimeout 和 setInterval

有时我们并不想立即执行一个函数,而是等待特定一段时间之后再执行。这就是所谓的“计划调用(scheduling a call)”。

目前有两种方式可以实现:

setTimeout 允许我们将函数推迟到一段时间间隔之后再执行。
setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。

这两个方法并不在 JavaScript 的规范中。但是大多数运行环境都有内建的调度程序,并且提供了这些方法。目前来讲,所有浏览器以及 Node.js 都支持这两个方法。

setTimeout

语法:

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)

参数说明:
func|code
想要执行的函数或代码字符串。 一般传入的都是函数。由于某些历史原因,支持传入代码字符串,但是不建议这样做。
delay
执行前的延时,以毫秒为单位(1000 毫秒 = 1 秒),默认值是 0;
arg1,arg2
要传入被执行函数(或代码字符串)的参数列表(IE9 以下不支持)

例如,在下面这个示例中,sayHi() 方法会在 1 秒后执行:

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);

传入一个函数,但不要执行它 新手开发者有时候会误将一对括号 () 加在函数后面:
// 错的! setTimeout(sayHi(), 1000);
// 错的! setTimeout(sayHi(), 1000); 这样不行,因为 setTimeout 期望得到一个对函数的引用。而这里的
sayHi() 很明显是在执行函数,所以实际上传入 setTimeout 的是 函数的执行结果。在这个例子中,sayHi() 的执行结果是
undefined(也就是说函数没有返回任何结果),所以实际上什么也没有调度

用 clearTimeout 来取消调度

setTimeout 在调用时会返回一个“定时器标识符(timer identifier)”,在我们的例子中是 timerId,我们可以使用它来取消执行。

取消调度的语法:

let timerId = setTimeout(...);
clearTimeout(timerId);

在下面的代码中,我们对一个函数进行了调度,紧接着取消了这次调度(中途反悔了)。所以最后什么也没发生:

let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // 定时器标识符clearTimeout(timerId);
alert(timerId); // 还是这个标识符(并没有因为调度被取消了而变成 null)

从 alert 的输出来看,在浏览器中,定时器标识符是一个数字。在其他环境中,可能是其他的东西。例如 Node.js 返回的是一个定时器对象,这个对象包含一系列方法。

我再重申一遍,这些方法没有统一的规范定义,所以这没什么问题。

针对浏览器环境,定时器在 HTML5 的标准中有详细描述

setInterval

setInterval 方法和 setTimeout 的语法相同:

let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)

所有参数的意义也是相同的。不过与 setTimeout 只执行一次不同,setInterval 是每间隔给定的时间周期性执行。

想要阻止后续调用,我们需要调用 clearInterval(timerId)

下面的例子将每间隔 2 秒就会输出一条消息。5 秒之后,输出停止:

// 每 2 秒重复一次
let timerId = setInterval(() => alert('tick'), 2000);// 5 秒之后停止
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);

alert 弹窗显示的时候计时器依然在进行计时 在大多数浏览器中,包括 Chrome 和 Firefox,在显示
alert/confirm/prompt 弹窗时,内部的定时器仍旧会继续“嘀嗒”。

所以,在运行上面的代码时,如果在一定时间内没有关掉 alert 弹窗,那么在你关闭弹窗后,下一个 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 灵活得多。采用这种方式可以根据当前执行结果来调度下一次调用,因此下一次调用可以与当前这一次不同。

例如,我们要实现一个服务(server),每间隔 5 秒向服务器发送一个数据请求,但如果服务器过载了,那么就要降低请求频率,比如将间隔增加到 10、20、40 秒等。

以下是伪代码:

let delay = 5000;let timerId = setTimeout(function request() {...发送请求...if (request failed due to server overload) {// 下一次执行的间隔是当前的 2 倍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 而言,内部的调度程序会每间隔 100 毫秒执行一次 func(i++):
在这里插入图片描述
注意到了吗?

使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短!

这也是正常的,因为 func 的执行所花费的时间“消耗”了一部分间隔时间。

也可能出现这种情况,就是 func 的执行所花费的时间比我们预期的时间更长,并且超出了 100 毫秒。

在这种情况下,JavaScript 引擎会等待 func 执行完成,然后检查调度程序,如果时间到了,则 立即 再次执行它。

极端情况下,如果函数每次执行时间都超过 delay 设置的时间,那么每次调用之间将完全没有停顿。

这是嵌套的 setTimeout 的示意图:
在这里插入图片描述
嵌套的 setTimeout 就能确保延时的固定(这里是 100 毫秒)。

这是因为下一次调用是在前一次调用完成时再调度的。

垃圾回收和 setInterval/setTimeout 回调(callback)

当一个函数传入 setInterval/setTimeout
时,将为其创建一个内部引用,并保存在调度程序中。这样,即使这个函数没有其他引用,也能防止垃圾回收器(GC)将其回收

// 在调度程序调用这个函数之前,这个函数将一直存在于内存中
setTimeout(function() {...}, 100);

对于 setInterval,传入的函数也是一直存在于内存中,直到 clearInterval 被调用。

这里还要提到一个副作用。如果函数引用了外部变量(译注:闭包),那么只要这个函数还存在,外部变量也会随之存在。它们可能比函数本身占用更多的内存。因此,当我们不再需要调度函数时,最好取消它,即使这是个(占用内存)很小的函数

零延时的 setTimeout

这儿有一种特殊的用法:setTimeout(func, 0),或者仅仅是 setTimeout(func)

这样调度可以让 func 尽快执行。但是只有在当前正在执行的脚本执行完成后,调度程序才会调用它。

也就是说,该函数被调度在当前脚本执行完成“之后”立即执行。

例如,下面这段代码会先输出 “Hello”,然后立即输出 “World”:

setTimeout(() => alert("World"));alert("Hello");

第一行代码“将调用安排到日程(calendar)0 毫秒处”。但是调度程序只有在当前脚本执行完毕时才会去“检查日程”,所以先输出 “Hello”,然后才输出 “World”。

零延时实际上不为零(在浏览器中)

在浏览器环境下,嵌套定时器的运行频率是受限制的。根据 HTML5 标准 所讲:“经过 5 重嵌套定时器之后,时间间隔被强制设定为至少 4 毫秒”。

让我们用下面的示例来看看这到底是什么意思。其中 setTimeout 调用会以零延时重新调度自身的调用。每次调用都会在 times 数组中记录上一次调用的实际时间。那么真正的延迟是什么样的?让我们来看看:

let start = Date.now();
let times = [];setTimeout(function run() {times.push(Date.now() - start); // 保存前一个调用的延时if (start + 100 < Date.now()) alert(times); // 100 毫秒之后,显示延时信息else setTimeout(run); // 否则重新调度
});// 输出示例:
// 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 毫秒以上的强制延时。(译注:这里作者没说清楚,timer 数组里存放的是每次定时器运行的时刻与 start 的差值,所以数字只会越来越大,实际上前后调用的延时是数组值的差值。示例中前几次都是 1,所以延时为 0)

如果我们使用 setInterval 而不是 setTimeout,也会发生类似的情况:setInterval(f) 会以零延时运行几次 f,然后以 4 毫秒以上的强制延时运行。

这个限制来自“远古时代”,并且许多脚本都依赖于此,所以这个机制也就存在至今。

对于服务端的 JavaScript,就没有这个限制,并且还有其他调度即时异步任务的方式。例如 Node.js 的 setImmediate。因此,这个提醒只是针对浏览器环境的。

总结

  • setTimeout(func, delay, ...args)setInterval(func, delay, ...args)方法允许我们在 delay 毫秒之后运行 func 一次或以 delay 毫秒为时间间隔周期性运行 func。
  • 要取消函数的执行,我们应该调用 clearInterval/clearTimeout,并将 setInterval/setTimeout 返回的值作为入参传入。
  • 嵌套的 setTimeout 比 setInterval 用起来更加灵活,允许我们更精确地设置两次执行之间的时间。
  • 零延时调度 setTimeout(func, 0)(与 setTimeout(func) 相同)用来调度需要尽快执行的调用,但是会在当前脚本执行完成后进行调用。
  • 浏览器会将 setTimeout 或 setInterval 的五层或更多层嵌套调用(调用五次之后)的最小延时限制在 4ms。这是历史遗留问题

请注意,所有的调度方法都不能 保证 确切的延时

例如,浏览器内的计时器可能由于许多原因而变慢:

CPU 过载。
浏览器页签处于后台模式。
笔记本电脑用的是省电模式。
所有这些因素,可能会将定时器的最小计时器分辨率(最小延迟)增加到 300ms 甚至 1000ms,具体以浏览器及其设置为准

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/620812.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【优选算法专栏】专题四:前缀和(二)

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…

外观模式:简化复杂系统的统一接口

在面向对象的软件开发中&#xff0c;外观模式是一种常用的结构型设计模式&#xff0c;旨在为复杂的系统提供一个简化的接口。通过创建一个统一的高级接口&#xff0c;这个模式帮助客户端通过一个简单的方式与复杂的子系统交互。本文将详细介绍外观模式的定义、实现、应用场景以…

网络管理实验二、SNMP服务与常用的网管命令

1 常用的网管命令 1.1 网络状态监视命令 包括以下命令&#xff1a;Ipconfig、ping、nslookup、dig、host ipconfig 作用&#xff1a;用来显示本机所有网卡的基本信息&#xff08;IP、掩码、网关、工作状态&#xff09;&#xff1b;用法&#xff1a;ipconfig展示&#xff1a;…

分类算法(数据挖掘)

目录 1. 逻辑回归&#xff08;Logistic Regression&#xff09; 2. 支持向量机&#xff08;Support Vector Machine, SVM&#xff09; 3. 决策树&#xff08;Decision Tree&#xff09; 4. 随机森林&#xff08;Random Forest&#xff09; 5. K近邻&#xff08;K-Nearest …

ARM/X86+FPGA轨道交通/工程车辆行业的解决方案

深圳推出首条无人驾驶地铁—深圳地铁20号线&#xff0c;可以说是深圳地铁的一次开创性的突破。智能交通不断突破的背后&#xff0c;需要很严格的硬件软件等控制系 统&#xff1b;地铁无人驾驶意味着信号系统、通信系统、综合监控系统、站台屏蔽门工程等项目必须严格执行验收。…

浦大喜奔APP8.0智能升级,发力数字金融深化五大金融篇章服务

1. 浦大喜奔立足科技赋能持续迭代升级&#xff0c;筑牢用户体验护城河 浦发信用卡中心坚持数字科技与客户体验双轮驱动&#xff0c;以科技赋能发展&#xff0c;优化整体系统性能&#xff0c;全方位支撑浦大喜奔 APP提高线上客户服务能力与体验&#xff0c;积极服务民生消费&a…

项目——boost搜索引擎

今天我们来写一个boost搜索引擎&#xff01; &#xff08;后续如果有更新&#xff0c;这个博客也会更新&#xff09; gitee连接:boost搜索引擎: boost搜索引擎 首先我们要介绍一下我们这个项目&#xff0c;我们项目的目的是通过我们的搜索引擎能够通过关键字查找到对应的网页…

如何通过Linux pciehp sysfs接口控制PCIe Slot电源状态?-2

NVME SSD电源状态判断 通过pciehp sysfs接口对NVMe SSD所在的PCIe插槽进行Power On/Off操作时&#xff0c;确实会间接影响到NVMe SSD本身的电源状态。因为NVMe SSD是作为PCIe设备连接到特定插槽上的&#xff0c;插槽电源状态的变化通常会直接影响到与其相连的设备。 当对PCIe…

缓存雪崩、缓存击穿、缓存穿透

缓存雪崩、缓存击穿、缓存穿透 文章目录 缓存雪崩、缓存击穿、缓存穿透缓存雪崩缓存击穿缓存穿透 缓存雪崩 当大量缓存数据在同一时间过期&#xff08;失效&#xff09;或者Redis宕机&#xff0c;如果此时有大量的用户请求&#xff0c;都无法在 Redis 中处理&#xff0c;于是全…

Http响应报文介绍

所有HTTP消息(请求与响应)中都包含&#xff1a; 一个或几个单行显示的消息头(header)&#xff0c; 在消息头部分主要包含&#xff1a;响应行信息和响应头信息 一个强制空白行&#xff1b; 最后是响应消息主体&#xff1b; 以下是一个典型的HTTP响应: HTTP/1.1 200 OK -- 响…

多叉树题目:收集树上所有苹果的最少时间

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;收集树上所有苹果的最少时间 出处&#xff1a;1443. 收集树上所有苹果的最少时间 难度 6 级 题目描述 要求 给定一个有 n \texttt{n} n 个结点的…

Linux系统启动过程详解

启动过程是指计算机从开机自检到操作系统完全加载的一系列动作。深入理解启动过程对于有效解决启动问题、提升系统性能以及高效管理系统的启动组件至关重要。例如&#xff0c;可以帮助我们识别和处理在启动过程中可能出现的诸如硬件故障、配置错误等问题。例如帮助我们个性化定…