当前位置: 首页 > 后端技术 > Node.js

setTimeout 或者 setInterval,关于 Javascript 计时器:你需要知道的一切都在这里

时间:2023-04-03 23:06:40 Node.js

setTimeout或setInterval,关于Javascript定时器:你需要知道的都在这里,你在哪里可以找到setTimeout的源代码(同样的问题是你在哪里可以看到setInterval的源代码)?很多时候,我们脑海中第一个闪过的答案肯定是V8引擎或者其他VM,但是要知道的是,我们所看到的所有Javascript计时函数都没有出现在ECMAScript标准中,也没有被任何Javascript引擎实现,计时功能实际上是由浏览器(或其他运行时,如Node.js)实现的,它在不同运行时下的表现可能不一致。在浏览器中,主要的定时器函数是Window接口的一部分,这保证了定时器函数如setTimeout、setInterval等函数和对象是全局可访问的,这就是为什么你可以随时随地使用setTimeout的原因。同样,在Node.js中,setTimeout是全局对象的一部分,可以像在浏览器中一样随时随地使用。到现在,可能有人会觉得这个问题没有实际价值,但是作为一个Javascript开发者,如果你不了解本质,那么你可能无法完全理解V8(或其他VM)是如何与浏览器进行交互的.或者Node.js交互。暂停函数的执行定时器函数是高阶函数,可用于暂停函数的执行,或使函数重复执行(执行其第一个参数需要执行的函数)。下面是一个暂停的例子:setTimeout(()=>{console.log('4secondshavepassedsincethefunctionwascalled')},4*1000)在上面的例子中,setTimeout会将console.log的执行暂停4*1000毫秒,即4秒。setTimeout的第一个函数是需要暂停的函数。它是对函数的引用。下面的例子是比较常见的写法:constfn=()=>{console.log('函数被调用已经过了4秒')}setTimeout(fn,4*1000)传递参数如果函数setTimeout暂停需要接收参数,我们可以使用第三个参数开始添加需要传递给暂停函数的参数:constfn=(name,gender)=>{console.log(`I'm${name},我是${gender}`)}setTimeout(fn,4*1000,'陶潘','男')上面的setTimeout调用,结果类似于下面的调用:setTimeout(()=>{fn('TaoPan','male')},4*1000)但是记住,结果是相似的,但本质上是不同的。我们可以用伪代码来表达setTimeout的函数实现:constsetTimeout=(fn,delay,...args)=>{wait(delay)//这里表示等待delay指定的毫秒数fn(...args)}写一个函数的挑战:当延迟为4秒时,它会打印出:自函数调用以来已经过了4秒。当延迟为8秒时,打印出:自函数调用以来已过去8秒。当延迟为N秒时,打印出:自函数调用以来已过去N秒。下面是我的一个实现:constdelayLog=delay=>{setTimeout(console.log,delay*1000,`距离函数调用已经过了${delay}秒`)}delayLog(4)//Output:Thedistance函数调用已通过4秒过去了delayLog(8)//输出:从函数调用开始已经过去了8秒方法setTimeout计算它的第三个参数距离函数的调用,${delay}秒过去了得到距离函数的调用,4秒过去了。setTimeout将计算出的字符串作为第一个参数console.log.log('函数被调用已经4秒了')执行,输出结果定时重复执行一个函数,停止重复调用如果我们要打印怎么办现在每4秒一次?有很多方法可以实现这一点。如果我们仍然使用setTimeout来实现,我们可以这样做:constloopMessage=delay=>{setTimeout(()=>{console.log('这里是loopMessage打印的消息')loopMessage(delay)},delay*1000)}loopMessage(1)//这个时候每隔1秒就会打印出一条消息:*这里是loopMessage打印的消息*但是有个问题,启动之后我们就没有办法停止了,what我们应该做什么?您可以稍微更改实现:letloopMessageTimerconstloopMessage=delay=>{loopMessageTimer=setTimeout(()=>{console.log('这是loopMessage打印的消息')loopMessage(delay)},delay*1000)}loopMessage(1)clearTimeout(loopMessageTimer)//我们可以随时使用`clearTimeout`来清除这个循环,但是仍然有一个问题。如果多次调用loopMessage,那么它们将共享一个loopMessageTimer。清除一个就清除所有,这是肯定不行的,所以我们要重新修改:constloopMessage=delay=>{lettimerconstlog=()=>{timer=setTimeout(()=>{console.log(`每${delay}秒打印一次)log()},delay*1000)}log()return()=>clearTimeout(timer)}constclearLoopMessage=loopMessage(1)constclearLoopMessage2=loopMessage(1.5)clearLoopMessage()//我们可以随时取消任何重复的调用而不影响其他...实现实现,但还有其他更好的解决方案:consttimer=setInterval(console.log,1000,'Printevery1second')clearInterval(timer)//`clearInterval`可以随时更深入地清除以便理解取消定时器(CancelTimers),上面的例子简单的给我们展示了setTimeout和setInterval,我们也可以看出我们可以通过clearTimeout或者clearInterval来取消定时器,但是关于定时器,远不止这些知识,请看下面的代码(please):constcancelImmediate=()=>{consttimerId=setTimeout(console.log,0,'Executionsuspendedfor0seconds')clearTimeout(timerId)}cancelImmediate()//不会有任何输出或查看以下代码:constcancelImmediate2=()=>setTimeout(console.log,0,'Executionsuspendedfor0seconds')consttimerId=cancelImmediate2()clearTimeout(timerId)请更改以上任何代码片段复制到浏览器控制台同时执行(多行copymultiplelines),你会发现这两个代码片段都没有任何输出,这是为什么?这是因为,由于Javascript的运行机制,任何时候只能有一个任务在进行。虽然我们调用了0秒的延时,但是由于当前任务还没有执行完,setTimeout中的挂起函数即使时间到了,也不会执行。您必须等到当前任务执行完毕。然后,再试一次,把上面的代码分支复制到控制台,看看结果会不会打印出执行已经暂停了0秒?答案是肯定的。当你逐行复制执行时,执行完cancelImmediate2后,当前任务全部执行完毕,于是开始执行下一个任务(console.log开始执行)。从上面的例子我们可以看出,setTimeout实际上是将一个任务安排到一个Javascript任务队列中。前面的任务全部执行完后,如果任务时间到了,则立即执行,否则,继续等待定时器超时。此时你应该发现,只要setTimeout暂停的函数还没有执行(任务还没有完成),那么我们随时可以使用clearTimeout来清除暂停(从队列中移除这个任务)。计时器是没有保证的。通过前面的例子我们知道,当setTimeout的delay为0时,并不代表马上执行。它必须等到所有当前的任务(对于一个JS文件来说,需要执行完当前脚本的所有调用)都执行完毕后才会执行,这其中就包括我们调用的clearTimeout。下面用一个例子来更清楚地说明这个问题:setTimeout(console.log,1000,'1秒后执行')//开始时间conststartTime=newDate()//从开始时间到现在过了多少秒letsecondsPassed=0while(true){//距离开始时间的毫秒数constduration=newDate()-startTime//如果距离开始时间超过5000毫秒,则终止循环if(duration>5000){break}else{//更新secondsPassedif(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秒以上才能完成所有当前任务。在它坏掉之前,所有其他操作都是无用的。当然,我们不会在开发过程中写这样的代码,但不代表不存在这样的情况。想象一下下面的场景:setTimeout(somethingMustDoAfter1Seconds,1000)openFileSync('filemorethen1gb')这里的openFileSync只是一个伪代码,意思是我们需要同步执行一个特别耗时的操作。这个操作很可能会超过1秒甚至更长,但是上面的somethingMustDoAfter1Seconds会一直处于pending状态。只要这个操作完成,就可以执行。为什么叫可能呢?那是因为可能还有其他任务会占用资源。因此,我们可以将setTimeout理解为:计时结束是任务执行的必要条件,但不是任务是否执行的决定性因素。setTimeout(somethingMustDoAfter1Seconds,1000)表示somethingMustDoAfter1Seconds必须在超过1000毫秒后执行。另一个小挑战。如果我需要每秒打印一个句子怎么办?从上面的例子可以看出setTimeout肯定解决不了这个问题。如果你不相信我,我们可以试试下面的代码片段:constlog=(delay)=>{timer=setTimeout(()=>{console.log(`每${delay}秒打印一次`)log(delay)},delay*1000)}log(1)上面的代码没有问题,在浏览器的控制台中观察,你会发现确实每秒打印一行,但是再试试这段代码:constlog=(delay)=>{timer=setTimeout(()=>{console.log(`每${delay}秒打印一次`)log(delay)},delay*1000)}constreadLargeFileSync=()=>{//开始时间conststartTime=newDate()//自开始时间以来经过的秒数letsecondsPassed=0while(true){//从开始时间开始的毫秒数constduration=newDate()-startTime//如果距离开始时间超过5000毫秒,则终止循环if(duration>5000){break}else{//如果距离开始时间增加一秒,更新secondsPassedif(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秒打印一次。第一秒,日志执行到1300毫秒的时候,开始执行readLargeFileSync,需要整整5秒。第2秒,log执行时间到,但是当前任务还没有完成,所以,第5秒不会打印,readLargeFileSync执行完成,所以log继续执行。如何实现这一点将不在本文讨论。到底是谁在调用挂起的函数呢?当我们在函数中调用this时,this关键字会指向当前函数的调用者:,将打印出窗口,当在Node.jsREPL中执行时,将执行全局,如果我们将whoCallsMe设置为对象的属性:functionwhoCallsMe(){console.log('Mycalleris:',this)}constperson={name:'TaoPan',whoCallsMe}person.whoCallsMe()这会打印:Mycalleris:Object{name:"TaoPan",whoCallsMe:whoCallsMe()}所以呢?functionwhoCallsMe(){console.log('Mycalleris:',this)}constperson={name:'TaoPan',whoCallsMe}setTimeout(person.whoCallsMe,0)这个会打印什么?这个容易被忽视的问题,其实很值得思考。请直接将以上代码片段复制到浏览器的控制台中查看执行结果:我的调用者是:Windowhttps://pantao.parcmg.com/admin/write-post.php?cid=2952然后打开系统终端,进入Node.jsREPL,执行同样的代码,看执行结果:_timerArgs:undefined,_repeat:null,_destroyed:false,[Symbol(refed)]:true,[Symbol(asyncId)]:221,[Symbol(triggerId)]:5}回到这句话:当我们在一个function,this关键字将指向当前函数的调用者。当我们使用setTimeout时,调用者与当前运行时相关。如果我希望this始终指向person对象怎么办?functionwhoCallsMe(){console.log('Mycalleris:',this)}constperson={name:'潘涛'}person.whoCallsMe=whoCallsMe.bind(person)setTimeout(person.whoCallsMe,0)结语标题上面写着你需要知道的都在这里了,但是如果有没有考虑到的地方,欢迎大家指出。