JavaScript定时器的更多知识点
导语:javascript定时器是window的一个对象接口,并不是javascript的一部分,它的功能是由浏览器实现的,在不同浏览器之间会有所不同。定时器也可以由node.js运行时本身实现。
几周前我在推特上发布了这样一个面试问题:
javascript面试问题:
在哪里可以找到settimeout和setinterval的源代码?(他们在哪里实现的?)
你怎么在面试中回答?(你不能去网上搜索)
function settimeout(callback

继续往下看之前先试着回答这个问题
推特上半数的回答都是错误的 回答不是 v8 (或者其他虚拟机!!)尽管著名的“javascript定时器”函数像settimeout
和 setinterval
都不是ecmascript规范或者任何javascript实现的一部分。 定时器功能由浏览器实现,它们的实现在不同浏览器之间会有所不同。定时器也可以由node.js运行时本身实现。
在浏览器里主要的定时器函数是作为window
对象的接口,window
对象同时拥有很多其他方法和对象。该接口使其所有元素在javascript全局作用域中都可用。这就是为什么你可以直接在浏览器控制台执行settimeout
。
在node里,定时器是global
对象的一部分,这点很像浏览器中的window
。你可以在node里看到定时器的源码 这里.
有些人可能认为这是一个糟糕的面试问题 - 为什么一定要知道这个问题呢?!作为一名javascript开发人员,我认为你应该知道这一点,因为如果你不这样做,那可能表明你并不完全理解v8(和其他虚拟机)如何与浏览器和node交互。
让我们开始做一些关于定时器函数的例子和挑战把?
更新: 这篇文章现在是“完整介绍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
里(放在已知的两个参数后)来中转参数给需要延迟执行的函数。
// for: func(arg1, arg2, arg3, ...) // we can use: 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”
使用您到目前为止学到的关于settimeout
的知识,在相应的延迟后打印以下2条消息。
- 4秒后打印消息“hello after 4 seconds”
- 8秒后打印“hello after 8 seconds”消息。
约束:您只能在解决方案中定义一个函数,其中包括内联函数。 这意味着许多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
调用也得到一个 第三个 参数来表示theonefunc
的delay
参数。
使用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”ms之后触发(使其立即生效),但它不会因为我们正在捕获timerid
值并在使用cleartimeout
调用后立即取消它。
当我们用node
命令执行example4.js
时,node不会打印任何东西,进程就会退出。
顺便说一句,在node.js中,还有另一种方法可以使用0
ms来执行settimeout
。 node.js计时器api有另一个名为setimmediate
的函数,它与settimeout
基本相同,带有0
ms但我们不必在那里指定延迟:
setimmediate( () => console.log('i am equivalent to settimeout with 0 ms'), );
setimmediate
方法在所有浏览器里都不支持。不要在前端代码里使用它。
就像cleartimeout
一样,还有一个clearinterval
函数,它对于setinerval
调用执行相同的操作,并且还有一个clearimmediate
调用。
在前面的例子中,您是否注意到在“0”ms之后执行带有settimeout
的内容并不意味着立即执行它(在settimeout行之后),而是在脚本中的所有其他内容之后立即执行它(包括cleartimeout调用)?
让我用一个例子清楚地说明这一点。 这是一个简单的settimeout调用,应该在半秒后触发,但它不会:
// example5.js settimeout( () => console.log('hello after 0.5 seconds. maybe!'), 500, ); for (let i = 0; i < 1e10; i++) { // block things synchronously }
在此示例中定义计时器之后,我们使用大的for
循环同步阻止运行时。 1e10
是1
后面有10
个零,所以循环是一个10
个十亿滴答循环(基本上模拟繁忙的cpu)。 当此循环正在滴答时,节点无法执行任何操作。
这当然是在实践中做的非常糟糕的事情,但它会帮助你理解settimeout
延迟不是一个保证的东西,而是一个最小的东西。 500
ms表示最小延迟为500
ms。 实际上,脚本将花费更长的时间来打印其问候语。 它必须等待阻塞循环才能完成。
编写脚本每秒打印消息“ hello world ”,但只打印5次。 5次之后,脚本应该打印消息“done”并让节点进程退出。
约束:你不能使用settimeout
调用来完成这个挑战。 提示:你需要一个计数器。
解决方案
以下是我如何解决这个问题:
let counter = 0; const intervalid = setinterval(() => { console.log('hello world'); counter += 1; if (counter === 5) { console.log('done'); clearinterval(intervalid); } }, 1000);
我将counter
值作为0
启动,然后启动一个setinterval
调用同时捕获它的id。
延迟功能将打印消息并每次递增计数器。 在延迟函数内部,if
语句将检查我们现在是否处于5
次。 如果是这样,它将打印“done”并使用捕获的intervalid
常量清除间隔。 间隔延迟为“1000”ms。
当你在常规函数中使用javascript的this
关键字时,如下所示:
function whocalledme() { console.log('caller is', this); }
this
关键字内的值将代表函数的调用者。 如果在node repl中定义上面的函数,则调用者将是global
对象。 如果在浏览器的控制台中定义函数,则调用者将是window
对象。
让我们将函数定义为对象的属性,以使其更清晰:
const obj = { id: '42', whocalledme() { console.log('caller is', this); } }; // the function reference is now: obj.whocallme
现在当你直接使用它的引用调用obj.whocallme
函数时,调用者将是obj
对象(由其id标识):
现在,问题是,如果我们将obj.whocallme
的引用传递给settimetout
调用,调用者会是什么?
// what will this print?? settimeout(obj.whocalledme, 0);
在这种情况下调用者会是谁?
答案根据执行计时器功能的位置而有所不同。 在这种情况下,你根本无法取决于调用者是谁。 你失去了对调用者的控制权,因为定时器实现将是现在调用您的函数的实现。 如果你在node repl中测试它,你会得到一个timetout
对象作为调用者:
请注意,这只在您在常规函数中使用javascript的this
关键字时才有意义。 如果您使用箭头函数,则根本不需要担心调用者。
编写脚本以连续打印具有不同延迟的消息“hello world”。 以1秒的延迟开始,然后每次将延迟增加1秒。 第二次将延迟2秒。 第三次将延迟3秒,依此类推。
在打印的消息中包含延迟时间。 预期输出看起来像:
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);
编写一个脚本以连续打印消息“hello world”,其具有与挑战#3相同的变化延迟概念,但这次是每个主延迟间隔的5个消息组。 从前5个消息的延迟100ms开始,接下来的5个消息延迟200ms,然后是300ms,依此类推。
以下是代码的要求:
- 在100ms点,脚本将开始打印“hello world”,并以100ms的间隔进行5次。 第一条消息将出现在100毫秒,第二条消息将出现在200毫秒,依此类推。
- 在前5条消息之后,脚本应将主延迟增加到200ms。 因此,第6条消息将在500毫秒+ 200毫秒(700毫秒)打印,第7条消息将在900毫秒打印,第8条消息将在1100毫秒打印,依此类推。
- 在10条消息之后,脚本应将主延迟增加到300毫秒。 所以第11条消息应该在500ms + 1000ms + 300ms(18000ms)打印。 第12条消息应打印在21000ms,依此类推。
- 一直重复上面的模式。
在打印的消息中包含延迟。 预期的输出看起来像这样(没有注释):
hello world. 100 // at 100ms hello world. 100 // at 200ms hello world. 100 // at 300ms hello world. 100 // at 400ms hello world. 100 // at 500ms hello world. 200 // at 700ms hello world. 200 // at 900ms hello world. 200 // at 1100ms...
约束:您只能使用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);
谢谢阅读。
上一篇: 首次模拟JS轮播图效果
下一篇: 我想学会的IDEA