JavaScript计时器:所有您需要知道的

大家好 曾几何时,约翰·雷齐格(John Rezig)撰写了有关哈布雷(Habré) 的文章 。 十年过去了,这个话题仍然需要澄清。 因此,我们为那些有兴趣阅读Samer Buna的文章的人,该文章不仅提供了JavaScript中的计时器的理论概述(在Node.js的上下文中),而且还提供了有关它们的任务。




几周前,我在一次采访中发布了以下问题:

“ setTimeout和setInterval函数的源代码在哪里? 你要去哪里找他 您无法在Google上搜索它:)“

***自己回答,然后继续阅读***



对此推文的回复中约有一半是不正确的。 不,这种情况与V8(或其他VM)无关! 诸如setTimeoutsetInterval类的功能(自豪地称为JavaScript JavaScript Timers)不属于任何ECMAScript规范或JavaScript引擎实现。 计时器功能是在浏览器级别实现的,因此它们的实现在不同的浏览器中会有所不同。 计时器也是在Node.js运行时本身中本地实现的。

在浏览器中,主要计时器功能是指Window界面,该界面也与其他一些功能和对象相关联。 该接口在JavaScript的主要范围内提供对其所有元素的全局访问。 这就是为什么可以在浏览器控制台中直接执行setTimeout函数的原因。

在Node中,计时器是global对象的一部分,该对象的设计类似于Window的浏览器界面。 这里显示了Node中计时器的源代码。

在某人看来,这只是面试中的一个坏问题-知道这一点有什么好处? 作为JavaScript开发人员,我这样想:假定您应该了解这一点,因为相反的情况可能表明您不太了解V8(和其他虚拟机)如何与浏览器和Node交互。

让我们看一些示例并解决几个计时器任务,我们来吗?

您可以使用node命令来运行本文中的示例。 我在Pluralsight 上的Node.js入门课程中介绍了此处讨论的大多数示例。

延迟执行功能

计时器是高阶函数,您可以使用它们延迟或重复执行其他功能(计时器接收的功能作为第一个参数)。

这是延迟执行的示例:

 // example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 ); 

在此示例中,使用setTimeout将问候消息延迟4秒。 setTimeout的第二个参数是延迟(以毫秒为单位)。 我将4乘以1000得到4秒。

setTimeout的第一个参数是一个函数,其执行将被延迟。
如果使用node命令执行example1.js文件,则Node会暂停4秒钟,然后显示一条欢迎消息(后面是退出消息)。

请注意: setTimeout的第一个参数只是一个函数引用 。 它不应是内置函数-例如example1.js 。 这是不使用内置函数的相同示例:

 const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000); 

传递参数

如果使用setTimeout延迟的函数接受任何参数,则可以使用setTimeout函数本身的其余参数(在我们已经研究过的2个参数之后)将参数的值传输到延迟函数。

 // : func(arg1, arg2, arg3, ...) //  : setTimeout(func, delay, arg1, arg2, arg3, ...) 

这是一个例子:

 // example2.js const rocks = who => { console.log(who + ' rocks'); }; setTimeout(rocks, 2 * 1000, 'Node.js'); 

上面的rocks函数延迟了2秒,它使用了who参数,并调用setTimeout将值“ Node.js”传递给了who参数。

当使用node命令执行example2.js时,将在2秒后显示短语“ Node.js rocks”。

计时器任务1

因此,基于已经研究过的有关setTimeout ,我们将在相应的延迟之后显示以下2条消息。

  • 4秒后将显示消息“ 4秒后您好”。
  • 8秒后将显示消息“ 8秒后您好”。

局限性

在您的解决方案中,您只能定义一个包含内置函数的函数。 这意味着许多setTimeout调用将必须使用相同的函数。

解决方案

这是我如何解决此问题的方法:

 // solution1.js const theOneFunc = delay => { console.log('Hello after ' + delay + ' seconds'); }; setTimeout(theOneFunc, 4 * 1000, 4); setTimeout(theOneFunc, 8 * 1000, 8); 

对我而言, theOneFunc接收delay参数,并在屏幕上显示的消息中使用此delay参数的值。 因此,该函数可以显示不同的消息,具体取决于我们将告知它的延迟值。

然后,我在两个setTimeout调用中使用了theOneFunc ,第一个调用在4秒后触发,第二个在8秒后触发。 这两个setTimeout调用也都接收一个第3个参数,表示theOneFuncdelay参数。

通过使用node命令执行solution1.js文件,我们将显示任务的要求,第一条消息将在4秒后出现,第二条消息将在8秒后出现。

重复功能

但是,如果我要您无限期每4秒显示一条消息怎么办?
当然,您可以setTimeout在一个循环中,但是计时器API还提供了setInterval函数,您可以使用该函数对任何操作的“永恒”执行进行编程。

这是setInterval的示例:

 // example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 ); 

此代码将每3秒显示一条消息。 如果使用node命令执行example3.js ,则Node将输出此命令,直到您强制结束该过程(CTRL + C)。

取消计时器

由于在调用计时器函数时分配了一个动作,因此在执行该动作之前也可以将其撤消。

setTimeout调用返回一个计时器ID,您可以在调用clearTimeout取消计时器时使用此计时器ID。 这是一个例子:

 // example4.js const timerId = setTimeout( () => console.log('You will not see this one!'), 0 ); clearTimeout(timerId); 

这个简单的计时器应该在0毫秒后(即立即)触发,但这不会发生,因为我们捕获了timerId的值,并通过调用clearTimeout立即取消了此计时器。

使用node命令执行example4.js时,Node将不会输出任何内容-该过程将立即结束。

顺便说一下,Node.js还提供了另一种setTimeout的方式,值为0 ms。 Node.js计时器API中还有另一个名为setImmediate ,它的功能基本上与setTimeout相同,但值为0 ms,但是在这种情况下,您可以省略延迟:

 setImmediate( () => console.log('I am equivalent to setTimeout with 0 ms'), ); 

setImmediate 所有浏览器都支持 setImmediate函数。 不要在客户端代码中使用它。

除了clearTimeout还有一个clearInterval函数,它的功能相同,但是有setInerval调用,还有一个clearImmediate调用。

计时器延迟-无法保证

您是否注意到在前面的示例中,使用setTimeout在0 ms之后执行操作时,此操作不会立即发生(在setTimeout之后),而是仅在所有脚本代码都已完全执行(包括clearTimeout调用)之后发生吗?

让我用一个例子阐明这一点。 这是一个简单的setTimeout调用,应该在半秒钟内起作用-但这不会发生:

 // example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { //    } 

在此示例中定义了计时器之后,我们立即使用大的for循环同步阻塞了运行时环境。 1e10值为1,带有10个零,因此该周期持续100亿个处理器周期(原则上,这模拟了处理器过载)。 在此循环完成之前,节点无法执行任何操作。

当然,实际上这是非常糟糕的,但是此示例有助于理解setTimeout延迟并不能保证,而是可以保证最小值 。 500 ms的值表示延迟将持续至少500 ms。 实际上,该脚本将需要更长的时间才能在屏幕上显示欢迎行。 首先,他将不得不等待直到阻塞周期完成。

计时器问题2

编写一个脚本,该脚本每秒显示一次“ Hello World”消息,但仅显示5次。 经过5次迭代后,脚本应显示“ Done”消息,此后Node进程将完成。

限制 :解决此问题时,不能调用setTimeout

提示 :需要柜台。

解决方案

这是我如何解决此问题的方法:

 let counter = 0; const intervalId = setInterval(() => { console.log('Hello World'); counter += 1; if (counter === 5) { console.log('Done'); clearInterval(intervalId); } }, 1000); 

我将0设置为counter的初始值,然后调用setInterval ,它获取其ID。

延迟功能将显示一条消息,并且每次将计数器增加一。 在deferred函数内部,我们有一个if语句,它将检查是否已经经过5次迭代。 5次迭代后,程序将显示“ Done”,并使用捕获的intervalId常数清除间隔值。 间隔延迟为1000毫秒。

谁确切地称为延迟函数?

使用JavaScript时,在常规函数中使用this ,例如:

 function whoCalledMe() { console.log('Caller is', this); } 

this的值将与调用方匹配。 如果在Node REPL内定义上述函数,则global对象将调用它。 如果在浏览器控制台中定义一个函数,则window对象将调用它。

让我们定义一个函数作为对象的属性,以使其更加清晰:

 const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } }; //     : obj.whoCallMe 

现在,当我们在使用obj.whoCallMe函数时直接使用指向它的链接时, obj对象(由其id )将充当调用者:



现在的问题是:如果将obj.whoCallMe的链接传递给obj.whoCallMesetTimetout

 //       ?? setTimeout(obj.whoCalledMe, 0); 

在这种情况下,呼叫者是谁?

答案将根据执行计时器功能的位置而有所不同。 在这种情况下,对呼叫者是谁的依赖是完全不能接受的。 您将失去对调用方的控制,因为这将取决于计时器的实现,在这种情况下,计时器将调用您的函数。 如果您在Node REPL中测试此代码,则Timeout对象将成为调用方:



请注意:仅当在常规函数中使用JavaScript this时,这一点才重要。 使用箭头功能时,呼叫者根本不会打扰您。

计时器问题3

编写一个脚本,该脚本将以不同的延迟连续输出“ Hello World”消息。 从一秒的延迟开始,然后在每次迭代时将其增加一秒。 在第二次迭代中,延迟将为2秒。 在第三个-三个,依此类推。

在显示的消息中包括延迟。 您应该得到这样的内容:

Hello World. 1
Hello World. 2
Hello World. 3
...


限制 :只能使用const定义变量。 不使用let或var。

解决方案

由于此任务中延迟的持续时间是一个变量,因此您不能在此处使用setInterval ,但是可以在递归调用中使用setTimeout手动配置间隔执行。 使用setTimeout执行的第一个函数将创建下一个计时器,依此类推。

另外,由于您不能使用let / var ,所以我们没有计数器来增加每个递归调用的延迟。 相反,您可以使用递归函数的参数在递归调用期间执行增量。

解决此问题的方法如下:

 const greeting = delay => setTimeout(() => { console.log('Hello World. ' + delay); greeting(delay + 1); }, delay * 1000); greeting(1); 

计时器任务4

编写一个脚本,该脚本以与任务3中相同的延迟结构显示“ Hello World”消息,但是这次以5条消息为一组,并且该组将有一个主要的延迟间隔。 对于第一组5条消息,我们选择100 ms的初始延迟,接下来的-200 ms,第三次-300 ms,依此类推。

该脚本的工作方式如下:

  • 在100 ms时,脚本首次显示“ Hello World”,并以100 ms为间隔增加5次。 第一条消息将在100毫秒后出现,第二条消息将在200毫秒后出现,依此类推。
  • 在前5条消息之后,脚本应将主延迟增加200 ms。 因此,第6条消息将在500 ms + 200 ms(700 ms)之后显示,第7条消息-900 ms,第8条消息-在1100 ms之后显示,依此类推。
  • 10条消息后,脚本应将主延迟间隔增加300 ms。 在500 ms + 1000 ms + 300 ms(18000 ms)之后应显示第11条消息。 第12条消息应在2100毫秒后显示,依此类推。

根据此原则,程序应无限期地工作。

在显示的消息中包括延迟。 您应该得到这样的内容(无评论):

Hello World. 100 // 100
Hello World. 100 // 200
Hello World. 100 // 300
Hello World. 100 // 400
Hello World. 100 // 500
Hello World. 200 // 700
Hello World. 200 // 900
Hello World. 200 // 1100
...


限制 :您只能使用对setInterval调用(而不能使用setTimeout ),并且只能使用if

解决方案

由于我们只能使用setInterval调用,因此在这里我们需要使用递归,还需要增加下一个setInterval调用的延迟。 此外,我们需要if才能仅在对该递归函数进行5次调用后才能使此操作发生。

这是一个可能的解决方案:

 let lastIntervalId, counter = 5; const greeting = delay => { if (counter === 5) { clearInterval(lastIntervalId); lastIntervalId = setInterval(() => { console.log('Hello World. ', delay); greeting(delay + 100); }, delay); counter = 0; } counter += 1; }; greeting(100); 

感谢所有阅读它的人。

Source: https://habr.com/ru/post/zh-CN426709/


All Articles