在写 setTimeout
和 setInterval
代码时,你是否有想过一下几点:javascript
首先 setTimeout
和 setInterval
都不是ECMAScript
规范或者任何JavaScript
实现的一部分。它是由浏览器实现,而且在不一样的浏览器也会有所差别。定时器也能够由 Nodejs
运行时自己实现。前端
在浏览器中,定时器是 Window
对象下的 api,因此能够直接在控制台进行直接调用。java
在 Nodejs
中,定时器是 global
对象的一部分,这点和浏览器的 Window
相似。具体能够去查看下node-timers源码node
有些人确定会想,为何必定要了解这些糟糕无聊的原理,咱们只须要运用别人 api 进行开发不就能够了。很遗憾的告诉你,做为一名 JavaScript
开发人员,我认为若是你只是想一直作一个初级开发工程师,那么你能够不去了解,若是想要提高,若是不去了解,那可能代表你并不彻底理解V8(和其余虚拟机)如何与浏览器和Node交互。git
本文会经过案例来说解 JavaScript 定时器,还会讲解某条的一些面试题github
// eg1.js
setTimeout(
() => {
console.log('Hello after 4 seconds');
},
4 * 1000
);
复制代码
上面这个例子用 setTimeout
延时 4 秒打印问候语。 若是你在node环境执行 example1.js。Node将会暂停4秒而后打印问候语(接着退出)。面试
setTimeout
第一个参数function - 是你想要在到期时间(delay毫秒)以后执行的函数。【注意:】 setTimeout
的第一个参数只是一个函数引用。 它没必要像eg1.js
那样是内联函数。 这是不使用内联函数的相同示例:api
const func = () => {
console.log('Hello after 4 seconds');
};
setTimeout(func, 4 * 1000);
复制代码
setTimeout
第二个参数 delay - 延迟的毫秒数 (一秒等于1000毫秒),函数的调用会在该延迟以后发生。若是省略该参数,delay取默认值0,意味着“立刻”执行,或者尽快执行。不论是哪一种状况,实际的延迟时间可能会比期待的(delay毫秒数) 值长setTimeout
第三个参数 param1, ..., paramN 可选 附加参数,一旦定时器到期,它们会做为参数传递给 function/ 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”bash
使用您到目前为止学到的关于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
函数,这将完成永远作某事的要求。
// example3.js
setInterval(
() => console.log('Hello every 4 seconds'),
4000
);
复制代码
此示例将每4秒打印一次消息。 使用 node 命令执行 example3.js 将使 Node 永远打印此消息,直到你终止该进程.
对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
延迟不是一个保证的东西,而是一个最小的东西。 500ms表示最小延迟为500ms。 实际上,脚本将花费更长的时间来打印其问候语。 它必须等待阻塞循环才能完成。
推荐你们看一篇Node.js Event loop 原理 里面讲的很深。
编写脚本每秒打印消息“ 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关键字时才有意义。 若是您使用箭头函数,则根本不须要担忧调用者。
以1秒的延迟开始,而后每次将延迟增长1秒。 第二次将延迟2秒。 第三次将延迟3秒,依此类推。
在打印的消息中包含延迟时间。 预期输出看起来像:
Hello World. 1
Hello World. 2
Hello World. 3...
复制代码
【注意】你只能使用const来定义变量。 你不能使用 let 或 var。 咱们先进行分析以下:
setInterval
,但咱们能够在递归调用中使用setTimeout
手动建立一个间隔执行。 使用setTimeout
的第一个执行函数将建立另外一个计时器,依此类推。如下是解决问题的一种方法:
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 语句。
如下是一种解决办法
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 实现一个 repeat 方法,输入输出以下:
// 实现
function repeat (func, times, wait) {},
// 输入
const repeatFunc = repeat(alert, 4, 3000);
// 输出
调用这个 repeatedFunc ("hellworld"),会 alert4 次 helloworld, 每次间隔 3 秒
复制代码
某一种解决办法以下
function repeat(func, times, wait) {
return function () {
let timer = null
const args = arguments
let i = 0;
timer = setInterval(()=>{
while (i >= times) {
clearInterval(timer)
return
}
i++
func.apply(null, args)
}, wait)
}
}
复制代码
函数节流解释:对函数执行增长一个控制层,保证一段时间内(可配置)内只执行一次。此函数的做用是对函数执行进行频率控制,经常使用于用户频繁触发但能够以更低频率响应的场景
其中一种解决办法:
function debounce (fn, time) {
let first = true
let timer = null
return function (...args) {
if (first) {
first = false
fn.apply(this, args)
}
timer = setTimeout(() => {
fn.apply(this, args)
}, 100)
}
}
复制代码
谢谢阅读, 欢迎你们继续补充