从一个超时程序的设计聊聊定时器的方方面面

目录

如何设计一个靠谱的超时程序

JS引擎的运行机制是怎样的?

如何避免程序卡顿?

如何判断H5程序是从后台台恢复过来的?

如何理解定时器的丢弃行为?

在开发中如何选择使用合适的定时器?

有没有一键回收所有定时器的方法?

如何理解定时器中的this对象?

零超时定时器在冒泡链中的活用

能否写一个通用的、立马执行的、有总数限制的、时间间隔均等的定时器?

习题与答案

如何设计一个靠谱的超时程序?

企业项目开发中经常有这样一个逻辑场景:在界面上显示倒计时,时间到了给出提示,禁止用户操作。

这个逻辑,简单一点可以使用JS的定时器实现,每隔1秒钟检查一次剩余时间,时间到了终止计时给出提示,时间不到就更新计时界面。代码如下所示:

代码1

输出是这样的:

从输出看没有问题,但这个实现有很大问题。没有错误并不代表写对了。

在上面的代码,函数将产生一个间隔定时器。JS定时器共有三种:间隔定时器、超时定时器、立即定时器。后两者分别由setTimeout、setImmediate产生,这两个函数稍后再讲。

看一下的使用语法:

第一个参数是一个函数,可以是匿名函数,也可以是命名函数,上面作者所用乃是es6的匿名箭头函数。

下面的代码为什么在小游戏中不能运行?

在小游戏中,setTimeout第一个参数必须为function类型,第二点alert属于BOM方法,是window对象的方法,在小游戏中没有window对象。

第二个参数,顾名思义是毫秒,意即间隔多少毫秒执行一次参数1。因为是每间隔一段时间执行一次,所以起名为间隔计时器。

第三个及以后的参数是不定参数,是在定时器触发时向参数1传递的实参。

返回的是定时器ID,这个ID在单程度内是唯一的且是递增的。向函数传入定时器ID,便是清除了定时器,定时器便不再触发。超时之后如果忘记了清理,也有办法统一打扫,这个问题稍后再讲。

以上面的代码1为例,如果想向参数1传递两个参数,一个任务名称和一个人员数量,应该如何改写?

作者试写一个,代码如下所示:

代码2

在上面的代码中,是函数setInterval的不定参数args。是匿名函数参数1的形参。

输出是这样的:

还是那句话,没有错误不代表写对了。在代码1中,我们设定定时器每隔1秒触发一次,但在实际的运行过程中,无法保证每隔1秒执行一次。如果间隔时间无法保证,例如延后了,那么总执行时间就要长于允许的总时间。

在代码1中,我们看到有一行这样的注释:

如果于此处加入一段非常耗时的逻辑代码,如下所示,势必将大大增加定时器的执时时间。

上面的代码仅是模拟,但在实际的项目中,确实可能存在这样耗费资源的操作,这便会导致超时计算出现误差。但是作者说的问题,原因还不在这里。如下所示:

图1

设间隔定时器每隔10秒触发一次,但青色逻辑代码仅耗时6秒,在这种情况下逻辑代码并不会对定时器造成影响。间隔定时器的触发,是由主线程之外的线程管理高度的,时间到了,就塞到主线程里执行,并不管上一次的代码有没有执行完。

在图1中,如果青色逻辑的执行时间是10s,而定时器的预设间隔是6秒呢,逻辑代码是多少秒执行一次?

不少于10s。在具体的实例中,可能还存在其它耗时操作,合理的答案是不少于10s。

如果在逻辑代码中访问了临界资源,会不会因为多次定时器重叠触发造成程序死锁?

不会,JS程序是单线程的。后面应当触发执行的代码,会被前面的延后。

对于函数第二个参数的描述,确准一点应该这样讲:

并不能保证定时器代码每隔一定时间如期执行。在实际的项目开发中,经常会有接口轮询操作,即每隔一定时间向服务器发起一次查询操作。在这种场景中,就不合适了,取而代之的当是。伪代码如下:

函数的语法与是类似的:

两者的参数与返回类型都是一样的,详见间隔定时器的语法说明。不同之处在于,生成的是超时定时器,在指定时间触发,且仅执行一次。

这个轮询不能停止,可以这样改进一下:

在需要停止轮询的时候,调用函数即可。函数用于清除超时定时器。在JS引擎内部,都维护了一个定时ID集合,每个ID对应什么类型的定时器都是一清二楚的,实在没有必要存在两个清除定时器的方法,与是完全可以合二为一的。

在上面的代码中,无论注释处的执行多久,相隔时间都是3秒。貌似这样就解决了问题,实现了时间上的等距轮询。

但是,超时定时器的执行同样受到JS是单线程的限制,即使轮询代码是一样的,但不能保证其它地方在本次循环中没有新增的代码,所以使用模拟的间隔定时器,仍然不能保证相待的间隔时间。

回到本文开始的问题上,应该如何设计超时逻辑?

定时器的时间不可信任,就不能拿定时器来衡量时间。可以将当前时间来计算剩余时间,代码如下所示:

定时器启动之前,先计算好超时时间。在定时器代码中,每次都检查一下当前时间与超时时间。这样无论定时器如何偏差,时间总不会错。

使用时间计算超时,这种方案适用于对时间要求不是特别精准的场景。说其不精准,不但是由于时钟的校准受限于PC或手机设备本身,还有定时器的时间粒度,最后应该超时时如果定时器代码一直被延后一直不被执行,同样会有很大误差。所以,还有一种做法是(定时器启动前)从服务器拉取时间,取服务器的时间作为时间参考值;在改变用户数据的时候,每次都要做检验,不能相信前端传递过来的数据。

JS引擎的运行机制是怎样的?

JS这门语言最大的特征就是单线程与异步操作。一个JS程序,无论是H5页面,还是小游戏/小程序,主线程是一个单线程。主线程从起始处依次解析、执行代码,然后会不断的添加新的代码,循环执行,形成一代一代的代码代。

当遇到定时器代码时,记当下定时器的注册时间,并将定时器交给另一个异步线程管理。定时器管理线程,会在设定的时间将定时器代码推入主线程。推入并不意味着一定执行,这要看主线程是否空闲。

遇到交互操作时,例如通过键盘敲入了字符,或单击了鼠标,此时协线程会将按键回调函数、鼠标单击回调函数添加到主线程作业栈尾部。会推迟到下一代执行,也可能是下下代。交互操作的优先级比定时器要高,这与乔布斯设计iOS系统的思想如出一辙,先保证操作的流畅性。

定时器代码、交互操作代码都是异步代码。如果在定时器代码,或在交互回调函数中又添加了新的代码,相当于在主线程尾部又续接了新的代码码,整个主线程像一个雷达波不断扩大,又像一根节节草一节一节循环执行。可以把这一节,称为桢。

对于不同的引擎,可能有一些常规代码基本是定时、自动插入主线程的,例如浏览器引擎,过一段都要渲染页面,渲染代码是每桢必有的;又如一些游戏引擎,本身有loop机制,在每个循环中都要重绘屏幕。

如何避免程序卡顿?

由于JS是单线程的,没有专门负责渲染UI的线程,如果引擎长时间耗于某段执行超过200ms的代码,就会呈现卡顿现象。解决方法,就是要善用JS的异步机制。

在JS中,有一些方法可以实现“下一代执行这些代码”,按照被处理的优先级,从上向下依次是:

1,process.nextTick

process.nextTick是Nodejs的API。process.nextTick不会进入异步队列,它是Nodejs特有的接口,会被强制加入主线程的尾部。也因为没有进入异步线程,所以比接下来的其它方法,其执行时间都要靠前。附其使用方法,参数是一个函数:

但这个方法是一个双刃剑,如果在process.nextTick中嵌套了process.nextTick,那么异步代码就永久没有办法执行了,程序可能进入了永久的卡顿或死锁当中。

2,Promise

process.nextTick是Nodejs特有的,使用不当容易自伤,忘记它吧。Promise的优先级虽比process.nextTick低,便比其它异步方法都高。附Promise的基本用法:

在上面的代码,resolve与reject都是function类型,是Promise内部负责实现调用链传递的。使用Promise的方便之处在于,不必关心调用链如果流动,只须把每一步的代码处理好。

Promise在小游戏中是可以使用的。

3,setImmediate

语法是这样的:

立即定时器,相比、,它没有设置时间的参数。顾名思义,立即定时器即是立即执行。但在JS引擎中,没有立即执行,所以这里的立即执行,即是在下一代中执行。

在下面会提到这种用法,是这种用法的代替品,但是执行优先级比后者要高。

立即定时器是一个比较新的定时器,目前IE11/Edge支持、Nodejs支持,Chrome不支持。将用到的代码,在Chrome浏览器中测试,是无法执行的。在小游戏中也未实现这个接口。

4,requestAnimationFrame

这是一个特殊的setTimeout类型的定时器,它的语法是这样的:

它的调用时机与屏幕刷新是一致的。所有涉及到与界面UI更新的代码,放在requestAnimationFrame里会被优雅,既浪费CPU资源,又可以避免卡顿现象。卡顿对象是一种视觉上的错觉,是屏幕刷新迟钝了,不一定是屏幕未刷新,也可能是要刷新的数据没有被及时更新。

5,setTimeout(fn, 0)

因为setImmediate未被广泛实现,所以这种方法在单次延时执行的场景中便成了最佳选择。通过将超时时间设置为0,fn将在下一代循环中被执行。

综上所述,刷新屏幕UI,使用requestAnimationFrame是先选。在不涉及界面的情况下,处理相互依赖的并发操作,使用Promise是首选,其它情况下使用setTimeout(fn, 0)最简单。

如何判断H5程序是从后台台恢复过来的?

定时器的时间是一成不变的吗?

不是的。

HTML5规范规定最小延迟时间不能小于4ms,即x如果小于4,会被当做4来处理。不同浏览器的实现也不一样,比如,Chrome可以设置1ms,IE11/Edge是4ms。

PC上的Firefox、Chrome和Safari等浏览器,都会自动把未激活页面中的定时器间隔最小值改为1秒以上;而移动设备上的浏览器往往会直接冻结未激活页面上的所有定时器,以此节省CPU开销。

基于此,上文中使用定时器累积计算时间也是有问题的。

在移动设备上利用定时器会冻结这个特征,可以判断程序是不是进入后台了。如下所示:

上面这段代码,如果在微信开发者工具中测试,是没有效果的。在微信中经作者测试也没有预期效果。

定时器的丢弃行为

在上面的代码中,sleep相当于是一段相当耗费资源的操作,虽然在它之前一个间隔为1秒的定时器已经被注册了,但在它的执行过程中,定时器不会触发(JS是单线程);并且在它之后,也不会输出3个2。因为线程被阻塞,定时器没有在应该触发的时间被触发,看起来像是被丢弃了,这便是定时器的丢弃行为。从本质上看,丢弃行为和延时行为是一致的。

从这点来看,也说明定时器的时间是靠不住的。

在开发中如何选择使用合适的定时器?

在上面的代码中,方法1不会每隔5秒钟就执行一次showTime函数,它是在每次调用setTimeout后过5秒钟再去执行showTime函数。假设showTime函数的主体部分需要2秒钟执行完,那么整个函数则要每7秒钟才执行一次。而setInterval却没有被自己所调用的函数所束缚,它只是简单地每隔一定时间就重复执行一次那个函数。

如果要求在每隔一个固定的时间间隔后就精确地执行某动作,那么最好使用setInterval,而如果不想由于连续调用产生互相干扰的问题,尤其是每次函数的调用需要繁重的计算以及很长的处理时间,那么最好使用setTimeout。

换言之,如果间隔时间较长,使用setInterval基本没有问题;如果间隔时间较短,且上下可能存在数据依赖或资源竞争,当使用setTimeout。

下面的代码为什么在小游戏中不能运行?

在小游戏中,setTimeout第一个参数必须为function类型,第二点alert属于BOM方法,是window对象的方法,在小游戏中没有window对象。

有没有一键回收所有定时器的方法?

如果对定时器函数不加以处理,那么setInterval将会持续执行相同的代码,一直到程序窗口关闭,或者用户转到了另外一个页面为止。这可能会造成内存泄漏,严重影响用户体验。

setTimeout和setInterval返回的整数值是连续的,也就是说,第二个setTimeout方法返回的整数值,将比第一个的整数值大1。利用这个特性,可以设计出一个定时器批量回收函数。如下所示:

清理间隔定时器与此同法。

关于定时器中的this对象

由于JS的作用域机制,this对象总是飘忽不定。

在上面的代码中,this并非指定义所在的obj对象,this.x奇迹般的指向了对象外的x。当obj.y在1000毫秒后运行时,this所指向的已经不是obj了,而是全局环境。

解决这个问题,有三种方法。

方法一:

上面代码中,obj.y放在一个匿名函数之中,这使得obj.y在obj的作用域执行,而不是在全局作用域内执行,所以能够显示正确的值。

方法二:

上面的代码,是使用bind方法,将obj.y这个方法绑定在obj上面。

方法三:

这种方法更简洁,它利用了箭头函数可以保持当前作用域不变的特性,作用域不变,this对象当然也不会变化。

零超时定时器在冒泡链中的活用

将setTimeout第二个函数设置为0,便是零超时定时器。上文中曾提到过,使用它避免程序卡顿现象的发生。现在谈一谈它在BOM冒泡链中的活用方法。

例如,在H5开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,想让父元素的事件回调函数先发生,就可以用setTimeout(fn, 0)。

输出:

能否写一个通用的、立马执行的、有总数限制的、时间间隔均等的定时器?

上面是一个模拟的间隔定时器。调用代码示例仅调用3次。在实际的企业项目开发中,对于一些网络请求,可能需要尝试3次甚至多次。

习题

1,下面代码的输出是什么?

2,执行在面的代码块会输出什么?

3,下面代码执行之后会输出什么?

4,模拟打印一个数字时钟?

非常感谢繁忙之余阅读

如果本文和君口味,莫忘点赞与评论

伴君悦读一同成长唯我长愿

你可能发现了我十分喜欢用美女作封面

身为一枚高负债矮挫穷的草根屌丝,爱美之心或不当有

有姑娘问我,“你多重?”

“180”,我说

“你多高?”

“160”

“哈哈,这不横向发展了”~

她这一笑,我好伤心,我们不能成为朋友了~

自此认识美女就成了我生活的主要矛盾~

我和美女喝咖啡,请美女吃饭看话剧,当然这都美女付钱~

写个技术文章,也不忘拿美女做封面~

毕竟一天的码字生活太苦了,我需要快乐~

说到快乐,我有一个秘诀,就是麻小~

天大的问题,只要一盆麻小就解决了~

如果一盆解决不了,那就两盆~

昨天我又去绝味店,我想说来一斤麻小

我没说

“来一瓶可乐,零度的”

毕竟麻小太贵了,我太胖了~

石桥码农

2018年4月17日于北京通州1000平大办公室

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180417A1FM0X00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券