setTimeout的那些事

如果懂setTimeout,可以直接看第3节,前面两节也可以当段子看一下。 如果不是很懂setTimeout,看下1,2两节应该会有一些收获。

1 JavaScript运行环境

之前关于service worker介绍的文章中,这样描述了浏览器环境下Javascript环境:"每个页面的javascript运行主线程都是一个Boss"、"Boss很厉害,在页面上指点江山,呼风唤雨。但他有个局限:同一时刻只做一件事(单线程)"。

以上体现了Javascript在浏览器运行环境中的局限性,单线程。实际上,不仅是在浏览器环境中,在Nodejs环境中的javascript也是单线程的。在不使用其它新员工(webworker等)的情况下,JS是如何在单线程上处理复杂的操作和逻辑,以至于在用户看来可以同时响应不同的操作的呢?

我们还是以Boss来称呼javascript的主线程吧。Boss为了更多更快地处理用户的需求,会不停地接收任务来执行。为了进一步提交效率,他优先执行最紧急的任务(即刻要执行),如果你要和他说"等下(3秒后 / 如果有我点了按钮 / 如果收到了服务器的响应)帮我在控制台打一个log吧。",BOSS不会专门等着去执行你的需求,而是默默地把你这个"伪需求"记在一个"小本本"上,然后拍拍胸脯和你说:"我保证(I promise!)",接着继续做手头上的事,等BOSS手头上事情做完了,会从小本本上选择最早记录的没被执行的任务来执行。

BOSS能力和时间有限,能做的只有这么多了。他Promise会帮你做的任务肯定会做(只要他没有猝死。。),但时间上可能并不一定严格符合你的要求,毕竟小本本上可能不仅只有一条任务。

想严肃了解JavaScript运行环境的同学可以看一下《JavaScript运行机制详解:再谈Event Loop》

2 理解setTimeout

咳咳。。是时候严肃一下了,我们改一下以上的称呼方式:

  • JS主线程 => BOSS
  • 同步任务 => BOSS手头上正在做的任务
  • 异步任务(队列) => BOSS的小本本上的任务

setTimeout这个方法相信很多初学者都有过误解:让JS从现在开始,经过指定的时间后,执行相应的函数。

从方法名和大部分现象来看,很容易产生以上的误解。在我们理解了JS主线程的特点后,知道了它会优先完成同步任务,在同步任务执行过程中,不会执行其它任务。

实际上,setTimeout做的事情是:在指定delay时间后,将指定方法作为异步任务添加到异步任务队列中

所以,如果setTimeout的定时到了执行时间,JS主线程仍然还在执行同步任务,setTimeout所指定的方法并不会立刻执行。 更惨的是,即使JS主线程执行完了同步任务,也不一定会执行setTimeout指定的方法,因为异步任务队列中可能有更早加入的异步任务。

最惨的是,即使天时地利人和,到了定时的时间时,JS主线程空闲,异步任务队列中只有setTimeout执行的方法,这个方法的执行时间也并不是精确的delay时间(精确到毫秒),因为浏览器上的计时器精确度有限:(以下摘自《Javascript高级程序设计(第三版)》)

  • IE8及更早版本的计时器精度为15.625ms
  • IE9及更晚版本的计时器精度为4ms
  • Firefox和Safari的计时器精度大约为10ms
  • Chrome的计时器精度为4ms

纵使setTimeout有些不尽人意,但这些瑕疵在大部分情况下,用户无法感知出来。 很多时候,setTimeout真正意图不是用来满足强迫者的精确需求,而是一种态度,一种中华名族传承已久的谦让美德:"You can you up!"。贯彻了此精神的代码,会让整个JS运行环境和谐运行。

给setTimeout一句评价就是:"上善若水,水善利万物而不争。" -- 摘自《道德经》

3 setTimeout应用例子

3.1 替换setInterval来实现重复定时

setTimeout有个哥哥:setInterval。他哥看起来叼叼的,可以循环地每隔一个delay就向异步任务队列中添加一个任务。实际上setInterval用起来真地顺滑吗?以下YY一段setTimeout表哥的对话:

setTimeout: 欧妮桑 setInterval:纳泥? setTimeout:我发现你可能有bug! setInterval:我愚蠢的弟弟啊。。肯定是你使用的方法不对! setTimeout:考虑到JS运行环境的特点,你的定时方法可能会连续执行,之间没有预期的间隔。 setInterval:Too young too simple! 你是说JS主线程的步同任务执行时间很长,并且异步队列中只有我在往其中添加任务,导致我在异步队列中重复添加的任务没有及时被执行,然后JS主线程空闲后,我添加的多个任务就会连续执行,是吗? setTimeout:其实我想说的是。。。 setInterval:机智的为兄早就料到了这一点,于是我在往异步队列中添加任务的时候,特意检测了队列中是否已经有了我之前添加的任务,如果有的话,为兄就不再重复添加。 setTimeout:你说的那个检测机制我知道,我想说的是,当JS主线程中正在执行你添加的任务,如果此时异步任务队列为空,你再向队列中添加异步任务时,JS主线程执行完你上次添加的任务,会立刻执行你这次添加的任务。 setInterval:。。。。这是没办法的啊,我只能检测队列中的任务,没法检测正在执行的任务。You can you up? setTimeout:请看下面代码:

setTimeout(function() {
    doWhatYouWantTo();
    setTimeout(arguments.callee, 100)
}, 100);

setInterval: (捂眼。。。)好亮的代码!你赢了...

使用以上setTimeout链式调用的方式,可以保证在下一次定时器代码执行之前,至少要等待指定的时间间隔,避免连续的运行。

3.2 防止事件疯狂触发

除了点击这种单次事件,浏览器上有一些会疯狂触发的事件,例如onreaize事件。如果给这个事件绑定了处理函数,在浏览器窗口大小改变的时候会很高频地触发处理函数。如果处理函数中有DOM操作的话,对页面性能影响会很大,尤其是在IE浏览器中,甚至可能让浏览器崩溃。

如果你实在需要在这类事件上绑定操作DOM的函数,那么可以考虑一下限制一下事件执行的时间间隔,至少不要那么频繁。至于设置多少时间间隔,看具体场景和需求。以下代码是利用setTimeout来实现事件执行频率控制:

/**
 * 限制method执行频次,当方法100ms之内没有
 * 再次被调用时,才执行method方法
 * @param  {function} method  被限制的方法
 * @param  {Object} context method执行的上下文
 */
function throttle(method, context) {
    clearTimeout(method.tid);

    method.tid = setTimeout(function() {
        method.call(context);
    }, 100);
}

function fnResize() {
    console.log(111);
}

window.onresize = function() {
    throttle(fnResize);
}

3.3 IE下重新播放单次gif动画

有这样一个需求:设计给了一个gif动画,gif本身是单次播放的。产品要求页面加载时动画播放一次。后续用户只要鼠标hover到动画上,动画就重新播放一次。利用搜索引擎的强大功能,很快找到了实现方案:

$logo.on('mouseenter', function() { // hover时重新播放gif动画
    var $logoImg = $(this).find('img');
    $logoImg.attr('src', ''); // 将img的src置为空
    $logoImg.attr('src', _opt.logoImg); // 重新设置src为gif链接,以实现重新播放
});

在chrome等浏览器上验证没问题后,按照惯例,在IE上出问题了。。。gif并没有重新播放一次。 当时想的是,可能是IE反应太慢了,在src属性重置的那个间隔内,没有意识到这一点。于是就尝试加了个setTimeout,把重新设置src的操作丢到了异步任务队列中。

$logo.on('mouseenter', function() { // hover时重新播放gif动画
    var $logoImg = $(this).find('img');
    $logoImg.attr('src', '');
    setTimeout(function() { // IE下要这样搞,不然不能重新播放动画
        $logoImg.attr('src', _opt.logoImg);
    }, 0);
});

虽然没有从根本上理解为什么IE会这样,但是setTimeout已然解决了这个问题。。

3.4 blur事件延时生效

经常有这种场景:监控input或者textarea中文本的变化,然后触发某个事件处理程序。考虑到除了键盘输入,还有鼠标的粘贴和剪切操作,比较完整的监控输入内容改变的方法是:

// 响应键盘输入,粘贴和剪切事件
$('#input').on('keyup paste cut', function() {
    console.log($(this).val());
});

以上代码在键盘输入场景下,能够在控制台输入最新的输入框内文本。但是当使用鼠标右键操作进行粘贴或剪切时,控制台输入的文本内容是操作前的旧内容。为了获取操作后的新文本内容,可以将对文本的获取和处理放在setTimeout中延时执行:

// 响应键盘输入,粘贴和剪切事件
$('#input').on('keyup paste cut', function() {
    var $this = $(this);
    setTimeout(function(){ // 使鼠标粘贴和剪切时,输入框内内容为最新
        console.log($this.val());
    }, 0)
});

3.5 更多用法?

setTimeout能够影响代码的执行顺序和时机,合理使用能够让更重要的代码优先执行,也可以FIX某些场景下的奇怪的bug。上面只列举了4种应用的场景,更多的用法欢迎大家讨论和补充。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java工会

前端进阶攻略|最全的前端开源JS框架和库

1727
来自专栏phodal

前端写一个月的原生 Android 是怎样一种体验?

一个前端程序员的一个月原生 Android 开发体验。自从我写了 Android 应用后,上知乎的时间变得更长了。 自从我写了 Android 应用后,上知乎的...

20910
来自专栏华仔的技术笔记

React Native 初探

3226
来自专栏听雨堂

将自动通知窗体集成到类中

        在IE的右下角自动弹出一个通知窗口,几秒后慢慢消失,这个现在是很常见的js代码实现的功能,但是,我希望能够把这个功能集成起来,使用时尽量简化,所...

1737
来自专栏IT技术精选文摘

Facebook构建高性能Android视频组件实践之路

其他的视频新闻类型可以播放生成的视频,赞助商的信息,或者短动画。 CoreVideoComponent是一个有着最简特性的任何视频新闻都需要的MountSpec...

35310
来自专栏大数据钻研

前端开发面试题总结之——HTML

---- 相关知识点 web标准、 web语义化、 浏览器内核、 兼容性、 html5... 题目&答案 Doctype作用?严格模式与混杂模式如何区分?它们有...

3248
来自专栏java一日一条

优化 iOS 程序性能的 25 个方法

ARC(Automatic ReferenceCounting, 自动引用计数)和iOS5一起发布,它避免了最常见的也就是经常是由于我们忘记释放内存所造成的内存...

784
来自专栏程序员的SOD蜜

无需重新编译代码,在线修改表单

    最近在跟朋友一起讨论工作流系统中自定义表单的问题,这些表单用于流程节点的数据处理,比如在请假流程中设计一个请假单。为了使工作流具有很高的灵活性,往往需要...

2236
来自专栏ThoughtWorks

#TW好文集锦# GUI应用的若干问题和模式

GUI应用的若干问题和模式 文/李光磊 我们所开发的应用程序大多都需要提供一个图形用户界面(GUI)。关于GUI应用的架构设计, 已经有了很多模式, 比如Ma...

2797
来自专栏Gcaufy的专栏

WePY 在手机充值小程序中的应用与实践

wepyjs 发布了两个月了,中间经历了很多版本更新,也慢慢开始有一些用户选择 wepyjs 作为开发框架来开发小程序,比如一些线上小程序。因此我也将手机充值小...

3.3K2

扫码关注云+社区