摘要:关于定时器的源码在文件中,进入就关于定时器的一些设计解释,因为是做服务端代码,在内部等大部分事件都会创建一个定时器,任何时间都可能存在大量的定时器任务,所以设计一个高效的定时器是很有必要的。
博客文章地址
setTimeout与setIntervalsetTimeout 和 setInterval 是我们在 javaScript 中经常用到的定时器,setTimeout 方法用于在指定的毫秒数后调用函数或计算表达式,setInterval 可按照指定的周期不停的调用函数或计算表达式。
但是当我们要循环调用某任务时候,处了用 setInterval 指定周期外,我们也可以用函数中嵌套setTimeout 回掉自己来实现, 可以看下面一段代码
// A function myTimeout() { doStuff() setTimeout(myTimeout, 1000) } myTimeout() // B function myTimeout() { doStuff() } myTimeout() setInterval(myTimeout, 1000)
上面A, B 两个方法都是在循环执行 myTimeout 函数,可是它们之间有什么不同呢。我们大部分都知道这其实取决与 doStuff 所消耗的时间, 如下图所示如果 doStuff 消耗时间很短(实际中大部分消耗时间都很短很难有所察觉),两个方法效果近似
当doStuff是一个很复杂的计算,需要消耗很长时间时候,我们就可以分析出A 方法(用setTimeout回掉)能够保障每一次任务结束到下一次任务开始的时间间隔为我们预期的值,但是B(setInterval)却能保证任务开始到下一次任务开始之间的间隔为我们预期的值,(当然如果doStuff执行时间比我们预期间隔还长,setInterval 还有可能会直接放弃某次任务,这种罕见情况我们暂不考虑)
为了感受其中的差异,这里定义一个模拟任务执行的函数
function wait(time) { var start = Date.now() while(Date.now() - start < time){} }
wait什么也没做,但是却可以阻塞进程time毫秒的时间,然后我们定义 doStuff,让它每次执行阻塞进程500ms,而且可以输出间隔时间信息,以及本次执行结束到下次执行开始的时间间隔
function doStuff() { console.log("doStuff___start", new Date().getSeconds()) //每次输出当前的秒数 console.timeEnd("timeout") //每次输出这次执行与上一次执行结束的时间间隔 wait(500) console.time("timeout") }
然后我们分别运行A, B两种方法
/* * A方法 setTimeout */ // doStuff___start 36 // timeout: 1002.865966796875ms // doStuff___start 37 // timeout: 1004.380859375ms // doStuff___start 39 // timeout: 1001.550048828125ms // doStuff___start 40 // timeout: 1001.051025390625ms // doStuff___start 42 // timeout: 1001.637939453125ms /* * B方法 setInterval */ // doStuff___start 50 // timeout: 500.412109375ms // doStuff___start 51 // timeout: 500.51806640625ms // doStuff___start 52 // timeout: 500.099853515625ms // doStuff___start 53 // timeout: 499.873291015625ms // doStuff___start 54 // timeout: 500.439697265625ms
可以看到 A 方法(用setTimeout回掉),我们保证了每次进程结束到下一次进程开始的间隔为预期值,但是从每次进程开始的时间间隔(我们这里精确到了秒)是会改变的,而B 方法(setInterval)表现的和我们预期的相同,正好与A相反。
nodejs中的差异目前为止所以的表现都合理,至少很符合预期。可是当我在 nodejs(v8.1.4) 中测试时候,却发现不管我用 setTimeout 还是 setInterval ,他们总是能表现出同样的效果(都是上面A方法的效果【用setTimeout回掉】)。这一点让我很困惑,经过一番探究,在 nodejs 关于 timers 的代码中找到了答案。
nodejs 关于定时器的源码在 node/lib/timer 文件中,进入就关于定时器的一些设计解释,因为 node 是做服务端代码,在内部 TCP, I/O.. 等大部分事件都会创建一个定时器,任何时间都可能存在大量的定时器任务,所以设计一个高效的定时器是很有必要的。
nodejs实现定时器也很巧妙, 为了可以轻松取消添加事件,nodejs使用了双向链表将 timer 插入和移除操作复杂度降低,具体实现在 node/lib/internal/linkedlist.js 文件中, 链表缺点自然是去查找元素,但是node ,把同一个时间间隔的 timer 维护在同一个双向链表中,这样就不需要去查找,因为先插入的总是先执行,具体的分析可以参考这篇文章 通过源码解析 Node.js 中高效的 timer.
回归主题,在 nodejs 关于 timer 的源码下,我们可以找到执行定时器的代码
// setInterval 会返回 createRepeatTimeout 的返回值 exports.setInterval = function(callback, repeat, arg1, arg2, arg3) { ... return createRepeatTimeout(callback, repeat, args); } // createRepeatTimeout函数生成timer function createRepeatTimeout(callback, repeat, args) { repeat *= 1; // coalesce to number or NaN if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) repeat = 1; // 这里间隔如果小于1或者大于TIMEOUT_MAX(2^31-1)都会按照1计算 var timer = new Timeout(repeat, callback, args); timer._repeat = repeat; // 追加了_repeat属性表示要循环调用 ... return timer; } // 函数回掉时,可以看到执行时在ontimeout函数中 function tryOnTimeout(timer, list) { ... try { ontimeout(timer); threw = false; } finally { if (timerAsyncId !== null) { if (!threw) ... } ... } // ontimeout执行 function ontimeout(timer) { var args = timer._timerArgs; var callback = timer._onTimeout; if (typeof callback !== "function") return promiseResolve(callback, args[0]); if (!args) timer._onTimeout(); else { switch (args.length) { case 1: timer._onTimeout(args[0]); break; case 2: timer._onTimeout(args[0], args[1]); break; case 3: timer._onTimeout(args[0], args[1], args[2]); break; default: Function.prototype.apply.call(callback, timer, args); } } if (timer._repeat) // 追加timer rearm(timer); }
上面代码分析,可以看到追加循环调用是在 ontimeout 函数中,它里面一大堆判断参数个数的内容可以不管,最后的if(timer._repeat) rearm(timer)判断是否要循环调用,可以看到它是在上面 timer._onTimeout 执行完之后才去执行的。这和我们开始写的A方法(用setTimeout回掉)基本类似,至此在 nodejs 表现出的不同就可以理解了。
看 issues , 关于这个问题也有很多讨论,还是有不少人想把它改会我们熟悉的方式的
setTimeout interval should not include duration of callback
setInterval interval includes duration of callback
具体最后要怎样还是要看后面的版本修改了。
参考资料nodejs源码
setTimeout or setInterval?
JavaScript的setTimeout和setInterval的深入理解
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/88843.html
摘要:的单线程,与它的用途有关。特点的显著特点异步机制事件驱动。队列的读取轮询线程,事件的消费者,的主角。它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给引擎。 这两天跟同事同事讨论遇到的一个问题,js中的event loop,引出了chrome与node中运行具有setTimeout和Promise的程序时候执行结果不一样的问题,从而引出了Nodejs的...
摘要:主线程会暂时存储等异步操作,直接向下执行,当某个异步事件触发时,再通知主线程执行相应的回调函数,通过这种机制,避免了单线程中异步操作耗时对后续任务的影响。 背景 在研究js的异步的实现方式的时候,发现了JavaScript 中的 macrotask 和 microtask 的概念。在查阅了一番资料之后,对其中的执行机制有所了解,下面整理出来,希望可以帮助更多人。 先了解一下js的任务执...
摘要:事件触发线程主要负责将准备好的事件交给引擎线程执行。它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给引擎。 Fundebug经作者浪里行舟授权首发,未经同意请勿转载。 前言 本文我们将会介绍 JS 实现异步的原理,并且了解了在浏览器和 Node 中 Event Loop 其实是不相同的。 一、线程与进程 1. 概念 我们经常说 JS 是单线程执行的,...
我们讲述的是关于 ahooks 源码系列文章的第七篇,总结主要讲述下面几点: 巩固 React hooks 的理解。 学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。 注:本系列对 ahooks 的源码解析是基于v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见详情。 ...
摘要:概述本篇主要介绍的运行机制单线程事件循环结论先在中利用运行至完成和非阻塞完成单线程下异步任务的处理就是先处理主模块主线程上的同步任务再处理异步任务异步任务使用事件循环机制完成调度涉及的内容有单线程事件循环同步执行异步执行定时器的事件循环开始 1.概述 本篇主要介绍JavaScript的运行机制:单线程事件循环(Event Loop). 结论先: 在JavaScript中, 利用运行至...
阅读 1663·2021-11-12 10:35
阅读 1613·2021-08-03 14:02
阅读 2660·2019-08-30 15:55
阅读 2026·2019-08-30 15:54
阅读 738·2019-08-30 14:01
阅读 2424·2019-08-29 17:07
阅读 2249·2019-08-26 18:37
阅读 3029·2019-08-26 16:51