先来回答一下下面这个问题:对于 setTimeout(function() { console.log('timeout') }, 1000) 这一行代码,你从哪里可以找到 setTimeout 的源代码(同样的问题还会是你从哪里可以看到 setInterval 的源代码)?
很多时候,可以我们脑子里面闪过的第一个答案肯定是 V8 引擎或者其它 VM们,但是要知道的一点是,所有我们所见过的 Javascript 计时函数,都没有出现在 ECMAScript 标准中,也没有被任何 Javascript 引擎实现,计时函数,其实都是由浏览器(或者其它运行时,比如 Node.js)实现的,并且,在不同的运行时下,其表现形式有可能都不一致。
在浏览器中,主计时器函数是 Window 接口的一部分,这保证了包括如 setTimeout、setInterval 等计时器函数以及其它函数和对象能被全局访问,这才是你可以随时随地使用 setTimeout 的原因。同样的,在 Node.js 中,setTimeout 是 global 对象的一部分,这拿得你也可以像在浏览器里面一样,随时随地的使用它。
到现在可能会有一些人感觉这个问题其实并没有实际的价值,但是作为一个 Javascript 开发者,如果不知道本质,那么就有可能不能完全的理解 V8 (或者其它VM)是到底是如何与浏览器或者 Node.js 相互作用的。
计时器函数都是更高阶的函数,它们可以用于暂缓一个函数的执行,或者让一个函数重复执行(由他们的第一个参数执行需要执行的函数)。
下面这是一个暂缓执行的示例:
setTimeout(() => {
console.log('距离函数的调用,已经过去 4 秒了')
}, 4 * 1000)
在上面的示例中, setTimeout 将 console.log 的执行暂缓了 4 * 1000 毫秒,也就是 4 秒钟, setTimeout 的第一个函数,就是需要暂缓执行的函数,它是一个函数的引用,下面这个示例是我们更加常见到的写法:
const fn = () => {
console.log('距离函数的调用,已经过去 4 秒了')
}
setTimeout(fn, 4 * 1000)
如果被 setTimeout 暂缓的函数需要接收参数,我们可以从第三个参数开始添加需要传递给被暂缓函数的参数:
const fn = (name, gender) => {
console.log(`I'm ${name}, I'm a ${gender}`)
}
setTimeout(fn, 4 * 1000, 'Tao Pan', 'male')
上面的 setTimeout 调用,其结果与下面这样调用类似:
setTimeout(() => {
fn('Tao Pan', 'male')
}, 4 * 1000)
但是记住,只是结果类似,本质上是不一样的,我们可以用伪代码来表示 setTimeout 的函数实现:
const setTimeout = (fn, delay, ...args) => {
wait(delay) // 这里表示等待 delay 指定的毫秒数
fn(...args)
}
编写一个函数:
下面这个是我的一个实现:
const delayLog = delay => {
setTimeout(console.log, delay * 1000, `距离函数的调用,已经过去 ${delay} 秒了`)
}
delayLog(4) // 输出:距离函数的调用,已经过去 4 秒了
delayLog(8) // 输出:距离函数的调用,已经过去 8 秒了
我们来理一下 delayLog(4) 的整个执行过程:
如果我们现在要每 4 秒第印一次呢?这里面就有很多种实现方式了,假如我们还是使用 setTimeout 来实现,我们可以这样做:
const loopMessage = delay => {
setTimeout(() => {
console.log('这里是由 loopMessage 打印出来的消息')
loopMessage(delay)
}, delay * 1000)
}
loopMessage(1) // 此时,每过 1 秒钟,就会打印出一段消息:*这里是由 loopMessage 打印出来的消息*
但是这样有一个问题,就是开始之后,我们就没有办法停止,怎么办?可以稍稍改改实现:
let loopMessageTimer
const loopMessage = delay => {
loopMessageTimer = setTimeout(() => {
console.log('这里是由 loopMessage 打印出来的消息')
loopMessage(delay)
}, delay * 1000)
}
loopMessage(1)
clearTimeout(loopMessageTimer) // 我们随时都可以使用 `clearTimeout` 清除这个循环
但是这样还是有问题的,如果 loopMessage 被调用多次,那么他们将共用一个 loopMessageTimer,清除一个,将清除所有,这是肯定不行的,所以,还得再改造一下:
const loopMessage = delay => {
let timer
const log = () => {
timer = setTimeout(() => {
console.log(`每 ${delay} 秒打印一次`)
log()
}, delay * 1000)
}
log()
return () => clearTimeout(timer)
}
const clearLoopMessage = loopMessage(1)
const clearLoopMessage2 = loopMessage(1.5)
clearLoopMessage() // 我们在任何时候都可以取消任何一个重复调用,而不影响其它的
这…… 实现是实现了,但是其它有更好的解决办法:
const timer = setInterval(console.log, 1000, '每 1 秒钟打印一次')
clearInterval(timer) // 随时可以 `clearInterval` 清除
上面的示例只是简单的给我们展现了 setTimeout 以及 setInterval,也看到了,我们可以通过 clearTimeout 或者 clearInterval 取消计时器,但是关于计时器,远远不止这点知识,请看下面的代码(请):
const cancelImmediate = () => {
const timerId = setTimeout(console.log, 0, '暂缓了 0 秒执行')
clearTimeout(timerId)
}
cancelImmediate() // 这里并不会有任何输出
或者看下面这样的代码:
const cancelImmediate2 = () => setTimeout(console.log, 0, '暂缓了 0 秒执行')
const timerId = cancelImmediate2()
clearTimeout(timerId)
请将上面的的任一代码片段同时复制到浏览器的控制台中(有多行复制多行)执行,你会发现,两个代码片段都没有任何输出,这是为什么?
这是因为,Javascript 的运行机制导致,任何时刻都只能存在一个任务在进行,虽然我们调用的是暂缓 0 秒,但是,由于当前的任务还没有执行完成,所以,setTimeout 中被暂缓的函数即使时间到了也不会被执行,必须等到当前的任务完全执行完成,那么,再试着,上面的代码分行复制到控制台,看看结果是不是会打印出 暂缓了 0 秒执行 了?答案是肯定的。
当你一行一行复制执行的时候, cancelImmediate2 执行完成之后,当前任务就已经全部执行完成了,所以开始执行下一个任务(console.log 开始执行)。
从上面的示例中,我们可以看出,setTimeout 其实是将一个任务安排进一个 Javascript 的任务队列里面去,当前面的所有任务都执行完成之后,如果这个任务时间到了,那么就立即执行,否则,继续等待计时结束。
此时,你应该发现,只要是 setTimeout 所暂缓的函数没有被执行(任务还没有完成),那么,我们就可以随时使用 clearTimeout 清除掉这个暂缓(将这条任务从队列里面移除)
通过前面的例子,我们知道了 setTimeout 的 delay 为 0 时,并不表示立马就会执行了,它必须等到所有的当前任务(对于一个 JS 文件来讲,就是需要执行完当前脚本中的所有调用)执行完成之后都会执行,而这里面就包括我们调用的 clearTimeout。
下面用一个示例来更清楚了说明这个问题:
setTimeout(console.log, 1000, '1 秒后执行的')
// 开始时间
const startTime = new Date()
// 距离开始时间已经过去几秒
let secondsPassed = 0
while (true) {
// 距离开始时间的毫秒数
const duration = new Date() - startTime
// 如果距离开始时间超过 5000 毫秒了, 则终止循环
if (duration > 5000) {
break
} else {
// 如果距离开始时间增长一秒,更新 secondsPassed
if (Math.floor(duration / 1000) > secondsPassed) {
secondsPassed = Math.floor(duration / 1000)
console.log(`已经过去 ${secondsPassed} 秒了。`)
}
}
}
你们猜上面这段代码会有什么样的输出?是下面这样的吗?
1 秒后执行的
已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。
并不是这样的,而是下面这样的:
已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。
1 秒后执行的
怎么会这样?这是因为 while(true) 这个循环必须要执行超过 5 秒钟的时间之后,才算当前所有任务完成,在它 break 之前,其它所有的操作都是没有用的,当然,我们不会在开发的过程中去写这样的代码,但是并不表示就不存在这样的情况,想象以下下面这样的场景:
setTimeout(somethingMustDoAfter1Seconds, 1000)
openFileSync('file more then 1gb')
这里面的 openFileSync 只是一个伪代码,它表示我们需要同步进行一个特别费时的操作,这个操作很有可能会超过 1 秒,甚至更长的时间,但是上面那个 somethingMustDoAfter1Seconds 将一直处于挂起状态,只要这个操作完成,它才有可能执行,为什么叫有可能?那是因为,有可能还有别的任务又会占用资源。所以,我们可以将 setTimeout 理解为:计时结束是执行任务的必要条件,但是不是任务是否执行的决定性因素。
setTimeout(somethingMustDoAfter1Seconds, 1000) 的意思是,必须超过 1000 毫秒后,somethingMustDoAfter1Seconds 才允许执行。
那如果我需要每一秒钟都打印一句话怎么办?从上面的示例中,已经很明显的看到了,setTimeout是肯定解决不了这个问题了,不信我们可以试试下面这个代码片段:
const log = (delay) => {
timer = setTimeout(() => {
console.log(`每 ${delay} 秒打印一次`)
log(delay)
}, delay * 1000)
}
log(1)
上面的代码是没有任何问题的,在浏览器的控制台观察,你会发现确实每一秒钟都打印了一行,但是再试试下面这样的代码:
const log = (delay) => {
timer = setTimeout(() => {
console.log(`每 ${delay} 秒打印一次`)
log(delay)
}, delay * 1000)
}
const readLargeFileSync = () => {
// 开始时间
const startTime = new Date()
// 距离开始时间已经过去几秒
let secondsPassed = 0
while (true) {
// 距离开始时间的毫秒数
const duration = new Date() - startTime
// 如果距离开始时间超过 5000 毫秒了, 则终止循环
if (duration > 5000) {
break
} else {
// 如果距离开始时间增长一秒,更新 secondsPassed
if (Math.floor(duration / 1000) > secondsPassed) {
secondsPassed = Math.floor(duration / 1000)
console.log(`已经过去 ${secondsPassed} 秒了。`)
}
}
}
}
log(1)
setTimeout(readLargeFileSync, 1300)
输出结果是:
每 1 秒打印一次
已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。
每 1 秒打印一次
关于这个具体怎么实现,就不在本文讨论了
当我们在一个 function 中调用 this 时,this 关键字会指向当前函数的 caller:
function whoCallsMe() {
console.log('My caller is: ', this)
}
当我们在浏览器的控制台中调用 whoCallsMe 时,会打印出 Window,当在 Node.js 的 REPL 中执行时,会执行出 global,如果我们将 whoCallsMe 设置为一个对象的属性:
function whoCallsMe() {
console.log('My caller is: ', this)
}
const person = {
name: 'Tao Pan',
whoCallsMe
}
person.whoCallsMe()
这会打印出:My caller is: Object { name: "Tao Pan", whoCallsMe: whoCallsMe() }
那么?
function whoCallsMe() {
console.log('My caller is: ', this)
}
const person = {
name: 'Tao Pan',
whoCallsMe
}
setTimeout(person.whoCallsMe, 0)
这会打印出什么?这个很容易被忽视的问题,其实真的值得我们去思考。
请直接将上面这个代码片段复制进浏览器的控制台,看执行的结果:
My caller is: Window https://pantao.parcmg.com/admin/write-post.php?cid=2952
再打开系统终端,进入 Node.js REPL 中,执行同样的代码,看执行结果:
My caller is: Timeout {
_idleTimeout: 1,
_idlePrev: null,
_idleNext: null,
_idleStart: 7052,
_onTimeout: [Function: whoCallsMe],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(refed)]: true,
[Symbol(asyncId)]: 221,
[Symbol(triggerId)]: 5
}
回到这句话:当我们在一个 function 中调用 this 时,this 关键字会指向当前函数的 caller,当我们使用 setTimeout 时,这个 caller 是跟当前的运行时有关系的,如果我想 this 总是指向 person 对象呢?
function whoCallsMe() {
console.log('My caller is: ', this)
}
const person = {
name: 'Tao Pan'
}
person.whoCallsMe = whoCallsMe.bind(person)
setTimeout(person.whoCallsMe, 0)
由于 Web Worker 本质上是Web线程,因此你可以在其中无限循环而不阻塞主线程。这使你可以访问微秒级的时间分辨率。这对于在 Worker 中做出时间关键的决策是特别实用的,可以让主线程准确的知道什么时候合适
首先 了解下 setTimeout的浅层原理,setTimeout 是通过浏览器异步API执行,执行完成之后,回调交给宏任务,假设宏任务队列已经有其他任务,就会导致 setTimeout 回调执行延后,从而不精准
在JavaScript中,我们经常使用requestAnimationFrame、setTimeout、setInterval和setImmediate来控制代码的执行时机。它们各有特点和适用场景:requestAnimationFrame: requestAnimationFrame主要用于浏览器动画渲染。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!