我们知道在JS中定时器的时间总是不准的,但一般来说误差是可以接受的。
但如果网页切换到了后台,这个时候大多数浏览器出于性能的考虑会将这一误差拉大,如果此时定时器设置的是比如1s这样较短时间执行一次的话,将会使实际业务的误差越拉越大。至此,我们得另想他法来解决这个问题,而Web Worker便能很好地解决该问题。
关于浏览器中JS定时器为什么会产生误差,可参考文章:
setTimeout()全局函数 MDN文档
JS定时器不可靠的原因及解决方案
1 Web Worker介绍
在正式讲解解决方案之前,先来讲一下什么是Web Worker。如果你已经了解过Web Worker了,可以跳过这一章节。
我们知道浏览器中JS的执行都是单线程的,而Web Worker则是HTML规范中提供的可以让我们可以再开一个线程去执行任务。这使得至少因为主线程要执行别的代码而导致的定时器变慢的问题可以被解决,因为这个时候我们可以考虑初始化一个Worker来专门执行定时器,与页面的主线程隔离开来。
1.1 Web Worker类型
Web Worker接口共有三个实现类型:专用Worker、Share Worker、Service Worker。它们各自的初始化代码如下:
new Worker(url, [options]) // 专用worker
new SharedWorker(url, [options]) // Share Worker
// Service Worker
// 这里的ServiceWorkerContainer是一个接口类型,可以是navigator对象
ServiceWorkerContainer.serviceWorker.register(url, [options])
上面的url
参数是从我们要加载的JS代码的位置,遵循同源策略。加载过来的JS代码会独立运行在一个新的线程中。
这三种类型的Worker功能各不相同,下面会谈到。
1.2 Web Worker限制
浏览器JS之所以是单线程运行的一个重要原因是想避免多线程的数据不一致问题。那么既然我们创建的Worker是独立运行在一个新的线程中,自然也免不了这个限制。
在Worker中,它的全局作用域变成了WorkerGlobalScope
,这个对象少了很多API,比如DOM操作、alert()
方法都是没有的。
WorkerGlobalScope
是一个接口类型,上面三种类型的Worker分别又各自继承实现了它,比如专用Worker的全局作用域是DedicatedWorkerGlobalScope
。各个Worker的作用域中所含API各不相同。
这里就不详细介绍Worker的具体API了,感兴趣的可以参考以下博客:
2 使用Web Worker设置定时器
这里我们选择最常用的专用Worker即可:
const blob = new Blob(`setTimeout(() => {
// 业务代码写在这里
}, 1e3)`)
const url = URL.createObjectURL(blob)
const worker = new Worker(url)
// worker.terminate() 如果不使用了记得终止worker,以节省资源
通过Blob
对象,我们可以动态地完成一段代码注入到Worker中。
3 注意事项
其实使用Worker去设置定时器,本质上还是难逃浏览器的优化,因为浏览器对于定时器的执行就是会根据情况进行延迟执行。
这里可以参考这篇博客The most accurate way to schedule a function in a web browser。这篇博客是用英语写的,如果能看得懂的话强烈建议阅读一下原文。我在这里简单翻译一下。
博主使用了三种方式设置定时器:setTimeout
、requestAnimationFrame
和在Web Worker中设置setTimeout
。同时实验对象包括各种浏览器,移动端和桌面端、Chrome和Safari等等。同时也设置了多种条件,比如页面在视图内/外、以及在iframe中。
综合下来三种方式都有误差,但Web Worker这种方式误差最小,尤其是在cross-origin的iframe中效果达到最佳。
不过也要注意的是,Web Worker初始化也是需要时间的,这个时间花费同样也是在cross-origin的iframe中时最低,另外安卓系统这个花费最高,iOS这个花费最低,不过总体来说这个花费可以忽略不记,除非你需要立马执行这个timeout。
其中这篇博客有一个评论值得注意,他给出了如下代码:
const t0 = performance.now()
while (performance.now() - t0 < 250) {}
// 业务代码写在这
从准确性的角度出发,这个可以说是准确无误了,因为它完全没用到setTimeout
,也就不存在所谓浏览器对于后台页面的优化了,但是很明显这个性能消耗很高,因为本质上是一个轮询操作,甚至如果这段代码是放在主线程而非Worker内的话,还会阻塞主线程的执行。
综合看来,在浏览器中想要实现一个精准的定时器,并没有所谓的银弹,对于不同情况应该具体分析该使用哪种方式。