先来回答一下下面这个问题:对于 setTimeout(function() { console.log('timeout') }, 1000)
这一行代码,你从哪里能够找到 setTimeout
的源代码(一样的问题还会是你从哪里能够看到 setInterval
的源代码)?php
不少时候,能够咱们脑子里面闪过的第一个答案确定是 V8 引擎或者其它 VM们,可是要知道的一点是,全部咱们所见过的 Javascript 计时函数,都没有出如今 ECMAScript 标准中,也没有被任何 Javascript 引擎实现,计时函数,其实都是由浏览器(或者其它运行时,好比 Node.js)实现的,而且,在不一样的运行时下,其表现形式有可能都不一致。浏览器
在浏览器中,主计时器函数是 Window
接口的一部分,这保证了包括如 setTimeout
、setInterval
等计时器函数以及其它函数和对象能被全局访问,这才是你能够随时随地使用 setTimeout
的缘由。一样的,在 Node.js 中,setTimeout
是 global
对象的一部分,这拿得你也能够像在浏览器里面同样,随时随地的使用它。async
到如今可能会有一些人感受这个问题其实并无实际的价值,可是做为一个 Javascript 开发者,若是不知道本质,那么就有可能不能彻底的理解 V8 (或者其它VM)是究竟是如何与浏览器或者 Node.js 相互做用的。函数
计时器函数都是更高阶的函数,它们能够用于暂缓一个函数的执行,或者让一个函数重复执行(由他们的第一个参数执行须要执行的函数)。oop
下面这是一个暂缓执行的示例:post
setTimeout(() => { console.log('距离函数的调用,已通过去 4 秒了') }, 4 * 1000)
在上面的示例中, setTimeout
将 console.log
的执行暂缓了 4 * 1000
毫秒,也就是 4 秒钟, setTimeout
的第一个函数,就是须要暂缓执行的函数,它是一个函数的引用,下面这个示例是咱们更加常见到的写法:this
const fn = () => { console.log('距离函数的调用,已通过去 4 秒了') } setTimeout(fn, 4 * 1000)
若是被 setTimeout
暂缓的函数须要接收参数,咱们能够从第三个参数开始添加须要传递给被暂缓函数的参数:code
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) }
编写一个函数:
delay
为 4 秒的时候,打印出:距离函数的调用,已通过去 4 秒了 delay
为 8 秒的时候,打印出:距离函数的调用,已通过去 8 秒了 delay
为 N 秒的时候,打印出:距离函数的调用,已通过去 N 秒了 下面这个是个人一个实现:
const delayLog = delay => { setTimeout(console.log, delay * 1000, `距离函数的调用,已通过去 ${delay} 秒了`) } delayLog(4) // 输出:距离函数的调用,已通过去 4 秒了 delayLog(8) // 输出:距离函数的调用,已通过去 8 秒了
咱们来理一下 delayLog(4)
的整个执行过程:
delay = 4
setTimeout
执行4 * 1000
毫秒后, setTimeout
调用 console.log
方法setTimeout
计算其第三个参数 距离函数的调用,已通过去 ${delay} 秒了
获得 距离函数的调用,已通过去 4 秒了
setTimeout
将计算获得的字符串看成 console.log
的第一个参数console.log('距离函数的调用,已通过去 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 秒打印一次
log
执行readLargeFileSync
这会须要整整 5 秒钟的时间log
执行时间到了,可是当前任务并无完成,因此,它不会打印readLargeFileSync
执行完成了,因此 log
继续执行关于这个具体怎么实现,就不在本文讨论了
当咱们在一个 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)
标题是写上了 你须要知道的一切都在这里,可是若是有什么没有考虑到了,欢迎你们指出。