记一次定时器问题的优雅解决

1,924 阅读7分钟

   当我们谈到 使用并要信任 前端计时器的时候,我总会露出这个表情

当真?_当真表情

    雷军向你投来一句  are you ok ?

    其实主要是因为在复杂的场景中我们的定时器并不会向我们想象的方式运行,他总会产生一些问题:

一、时间误差

    我们想着时钟定时器能精确按秒来运行,但总是会有些偏差。那这些偏差是怎么来的呢?

    要找到问题的根源,我们需要了解浏览器运行的整个机制,啊,一提到整个运行机制,不会吧,不会吧,你还没了解。

网友听了想打人,又要学!!

    哎,其实我也没有,那么由我替你们肝吧。

    其实最主要的还是要熟悉浏览器的事件循环 (event loop)

    我们都知道js语言的特点是单线程,也就是说同一时间只能做一个任务

    有的同学肯定想问了,为啥不能是多线程,这样效率不就更高了嘛。那我反回一句,如果是多线程,一个线程要在这个dom节点上添加内容,另一个线程把这个dom节点删除了,请问浏览器该怎么办?接着浏览器打印了一句window.close()  拜拜了您嘞,您自个玩。JavaScript是浏览器脚本语言,主要用途是和用户交互互动,操作dom,多线程会增加复杂性,所以他注定要是单线程,这已经成了这门语言的核心特征,将来也不会改变。不过html5提出Web Worker标准,允许js创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。

    任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

异步任务还分为 微任务 和 宏任务,如下图所示:

  • 宏任务:由宿主对象发起的任务(setTimeout)
    • 宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering。* 微任务:由js引擎发起的任务(promise)
    • 微任务包括 process.nextTickpromiseMutationObserver

具体来说,异步执行的运行机制如下

1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,
就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。
那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。(
4)主线程不断重复上面的第三步。

   来, 祭出你们要的图。

     js引擎在主线程执行代码的时候,执行同步任务过程如果遇到异步任务将会把异步任务的结果放置在任务队列中,等到主线程的同步任务执行完毕,接下来js引擎会在任务队列中找出相对近的微任务放到执行栈中执行,微任务执行完毕后再去任务队列中找宏任务执行,然后主线程循环这样的过程。

       理想来说,setInterval 和setTimeout 会按照我们的想法按时执行回调,在执行到一定时间后停止当前任务去任务队列中找到回调函数并执行,但是 仅限于理想,setInterval 和setTimeout 每次调用都会花个大概 4ms 的时间,这也会造成延迟误差,但可以忽略不计,如果主线程执行代码花费的时候超过了 间隔时间,那他只能等到同步任务执行完毕后才会去执行会带哦,一直这样循环下去那他造成的误差将会变大。所以我们尽量不要在定时器回调中进行复制计算。

    关于Event Loop我就简单讲到这里。

    到这里了解完了事件循环,你以为完了?不,因为我在ios上还发现了一个更严重的问题

**二、IOS 伪后台导致计时器停止 **

      提到伪后台,我不得不再去了解了一下ios的后台机制。

      随着ios 4的发布 ,”伪后台“的称号呼之而来。

      在ios4前,我们按home键就直接把应用进程杀死了,我简称它为”无后台“。 

      在ios4后,当你点击Home键后,当前程序转入后台进入挂起(suspend)状态(保存在内存中但不执行任何代码),用户可以快速恢复。但是苹果对后台的限制非常严格,只有一些特殊应用可以在后台真正运行,比如音频播放类,VoIP类,newsstand,位置服务等。如果不做任何配置的话,你的应用最多只能在后台运行5秒,之后就会被挂起 (存于内存但不运行),如果你配置了后台运行(如需下载),大概在594秒(9.9分钟)时进程停止,程序同样也会被挂起,一旦系统需要内存了,最早的后台进程就会被杀掉以释放内存。

      随着ios7的发布,ios迎来的”智能后台“阶段。

     在iOS 7之前,如果应用在后台运行,那么即使你已锁屏了,这些应用还会继续运行,你的iPhone一直处于唤醒状态,直到后台应用完成任务或超时才能进入休眠。

**      在iOS 7之后一旦手机锁屏,后台应用将也会很快被暂停,之后当系统被唤醒时(比如接到电话),那些暂停的后台应用也会一起继续运行。**这样的好处是,系统不会因为第三方应用在后台运行而长时间处于唤醒状态,对电池续航有益,而且第三方应用的保持时间也更久了。

      后台机制我就讲到这里,感兴趣的同学可以google一下了解更加详细的内容。

     接下来说一下iOS中 亮屏后台切换到前台 的这两种场景我们前端该怎么处理,

     他们都会带来一个影响,就是会挂起我们的线程,导致我们的定时器不执行,当我们在切回到程序的时候会中间会有个空档期,例如我们切到后台一分钟再回来,发现定时器只走了5秒,还有55秒就是这个空档期。

我们需要去重新校准时间。思路如下:

1、在定时器中每次获取数据最新值。

2、在定时器中每次记录时间戳然后计算时间差,如果和时间间隔相差较大的话重新获取数据

3、在body.onblur 回调中处理.

**4、但是我一般会先初始化数据值然后在定时器中进行简单计算,尽量保持计时器的回调函数占用时间最短。把数据初始化化放在外面,就依赖一些触发时机。当我们切回页面到前台的时候,我们需要监听页面状态的方法,**经查阅MDN我找到以下方法:

document.onvisibilitychange = function// Document.onvisibilitychange 是一个事件处理方法,
它将在该对象的 visibilitychange事件被触发时调用。兼容性奉上:

  • document.addEventListener("visibilitychange", function() { console.log( document.visibilityState === 'visible' ); // 页面可见性
    // Modify behavior...
    }); // 浏览器标签页被显示或隐藏都会触发该事件,兼容性如下:

上面这些方法都能监听到页面从后台切到前台或者息屏亮屏。虽然存在一些兼容问题,但我们解决的是ios的 息屏和从后台切回的问题,对ios的兼容来看还是不错的。遇到兼容问题的话将在计时器中获取最新数据,获取时间间隔差,根据差值补上数据。

新人写文不易,各位大佬请重喷,如遇到有错误的地方请大佬指出。