JS定时器不可靠的原因及解决方案

目录
  • 前言
  • 定时器工作原理
  • setInterval调用被废弃
  • 事件循环
  • 导致定时器不可靠的原因
  • 延迟执行时间有最大值
  • 解决方案
  • 总结

前言

在工作中应用定时器的场景非常多,但你会发现有时候定时器好像并没有按照我们的预期去执行,比如我们常遇到的setTimeout(()=>{},0)它有时候并不是按我们预期的立马就执行。想要知道为什么会这样,我们首先需要了解Javascript计时器的工作原理。

定时器工作原理

为了理解计时器的内部工作原理,我们首先需要了解一个非常重要的概念:计时器设定的延时是没有保证的。因为所有在浏览器中执行的JavaScript单线程异步事件(比如鼠标点击事件和计时器)都只有在它有空的时候才执行。

这么说可能不是很清晰,我们来看下面这张图

图中有很多信息需要消化,但是完全理解它会让您更好地了解异步JavaScript执行是如何工作的。这张图是一维的:垂直方向是(挂钟)时间,单位是毫秒。蓝色框表示正在执行的JavaScript部分。例如,第一个JavaScript块执行大约18ms,鼠标点击块执行大约11ms,以此类推。

​ 由于JavaScript一次只能执行一段代码(由于它的单线程特性),所以每一段代码都会“阻塞”其他异步事件的进程。这意味着,当异步事件发生时(如鼠标单击、计时器触发或XMLHttpRequest完成),它将排队等待稍后执行。

​ 首先,在JavaScript的第一个块中,启动了两个计时器:一个10ms的setTimeout和一个10ms的setInterval。由于计时器是在哪里和什么时候启动的,它实际上在我们实际完成第一个代码块之前触发,但是请注意,它不会立即执行(由于线程的原因,它无法这样做)。相反,被延迟的函数被排队,以便在下一个可用的时刻执行。

​ 此外,在第一个JavaScript块中,我们看到鼠标单击发生。与此异步事件相关联的JavaScript回调(我们永远不知道用户何时会执行某个动作,因此它被认为是异步的)无法立即执行,因此,就像初始计时器一样,它被排队等待稍后执行。

​ 在JavaScript的初始块完成执行后,浏览器会立即问一个问题:等待执行的是什么?在本例中,鼠标单击处理程序和计时器回调都在等待。然后浏览器选择一个(鼠标点击回调)并立即执行它。计时器将等待到下一个可能的时间,以便执行。

setInterval调用被废弃

在click事件执行时,第20毫秒处,第二个setInterval也到期了,因为此时已经click事件占用了线程,所以setInterval还是不能被执行,并且因为此时队列中已经有一个setInterval正在排队等待执行,所以这一次的setInterval的调用将被废弃。

浏览器不会对同一个setInterval处理程序多次添加到待执行队列。

​ 实际上,我们可以看到,当第三个interval回调被触发时,interval本身正在执行。这向我们展示了一个重要的事实:interval并不关心当前执行的是什么,它们将不加区别地排队,即使这意味着回调之间的时间间隔将被牺牲。

setTimeout/setInterval无法保证准时执行回调函数

​ 最后,在第二个interval回调执行完成后,我们可以看到JavaScript引擎没有任何东西可以执行了。这意味着浏览器现在等待一个新的异步事件发生。当interval再次触发时,我们会在50ms处得到这个值。但是这一次,没有任何东西阻碍它的执行,因此它立即触发。

OK,总的来说造成JS定时器不可靠的原因就是JavaScript是单线程的,一次只能执行一个任务,而setTimeout() 的第二个参数(延时时间)只是告诉 JavaScript 再过多长时间把当前任务添加到队列中。如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行定时器任务必须等主线程任务执行才可能开始执行,无论它是否到达我们设置的时间

这里我们可以再来了解下Javascript的事件循环

事件循环

JavaScript中所有的任务分为同步任务与异步任务,同步任务,顾名思义就是立即执行的任务,它一般是直接进入到主线程中执行。而我们的异步任务则是进入任务队列等待主线程中的任务执行完再执行。

任务队列是一个事件的队列,表示相关的异步任务可以进入执行栈了。主线程读取任务队列就是读取里面有哪些事件。

队列是一种先进先出的数据结构。

上面我们说到异步任务又可以分为宏任务与微任务,所以任务队列也可以分为宏任务队列与微任务队列

  • Macrotask Queue:进行比较大型的工作,常见的有setTimeout,setInterval,用户交互操作,UI渲染等;
  • Microtask Queue:进行较小的工作,常见的有Promise,Process.nextTick;
  • 同步任务直接放入到主线程执行,异步任务(点击事件,定时器,ajax等)挂在后台执行,等待I/O事件完成或行为事件被触发。
  • 系统后台执行异步任务,如果某个异步任务事件(或者行为事件被触发),则将该任务添加到任务队列,并且每个任务会对应一个回调函数进行处理。
  • 这里异步任务分为宏任务与微任务,宏任务进入到宏任务队列,微任务进入到微任务队列。
  • 执行任务队列中的任务具体是在执行栈中完成的,当主线程中的任务全部执行完毕后,去读取微任务队列,如果有微任务就会全部执行,然后再去读取宏任务队列
  • 上述过程会不断的重复进行,也就是我们常说的事件循环(Event-Loop)。

这里更详细的内容可以看我之前的文章探索JavaScript执行机制

导致定时器不可靠的原因

当前任务执行时间过久

JS 引擎会先执行同步的代码之后才会执行异步的代码,如果同步的代码执行时间过久,是会导致异步代码延迟执行的。

setTimeout(() => {
  console.log(1);
}, 20);
for (let i = 0; i < 90000000; i++) { }
setTimeout(() => {
  console.log(2);
}, 0);

这个按预期应该是会先打印出2,然后再打印1,但事实并不是如此,就算第二个定时器的时间更短,但中间那个for循环的执行时间远远超过了这两个定时器设定的时间。

setTimeout 设置的回调任务是 按照顺序添加到延迟队列里面的,当执行完一个任务之后,ProcessDelayTask 函数会根据发起时间和延迟时间来计算出到期的任务,然后 依次执行 这些到期的任务。

在执行完前面的任务之后,上面例子的两个 setTimeout 都到期了,那么按照顺序执行就是打印 12。所以在这个场景下,setTimeout 就显得不那么可靠了。

延迟执行时间有最大值

包括 IE, Chrome, Safari, Firefox 在内的浏览器其内部以32位带符号整数存储延时。这就会导致如果一个延时(delay)大于 2147483647 毫秒 (大约24.8 天)时就会溢出,导致定时器将会被立即执行。(MDN)

setTimeout 的第二个参数设置为 0 (未设置、小于 0、大于 2147483647 时都默认为 0)的时候,意味着马上执行,或者尽快执行。

setTimeout(function () {
  console.log("你猜它什么时候打印?")
}, 2147483648);

把这段代码放到浏览器控制台执行,你会发现它会立马打印出 你猜它什么时候打印?

最小延时>=4ms(嵌套使用定时器)

在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的setInterval的回调函数阻塞导致的。

  • setTimeout 的第二个参数设置为 0 (未设置、小于 0、大于 2147483647 时都默认为 0)的时候,意味着马上执行,或者尽快执行。
  • 如果延迟时间小于 0,则会把延迟时间设置为 0。如果定时器嵌套 5 次以上并且延迟时间小于 4ms,则会把延迟时间设置为 4ms
function cb() { f(); setTimeout(cb, 0); }
setTimeout(cb, 0);

在Chrome 和 Firefox中, 定时器的第5次调用被阻塞了;在Safari是在第6次;Edge是在第3次。所以后面的定时器都最少被延迟了4ms

未被激活的tabs的定时最小延迟>=1000ms

浏览器为了优化后台tab的加载损耗(以及降低耗电量),在未被激活的tab中定时器的最小延时限制为1S(1000ms)。

let num = 100;
function setTime() {
  // 当前秒执行的计时
  console.log(`当前秒数:${new Date().getSeconds()} - 执行次数:${100-num}`);
  num ? num-- && setTimeout(() => setTime(), 50) : "";
}
setTime();

这里我在39秒时切到了其他标签页,我们会发现它后面的执行间隔都是1秒执行一次,并不是我们设定的50ms。

setInterval的处理时长不能比设定的间隔长

setInterval的处理时长不能比设定的间隔长,否则setInterval将会没有间隔的重复执行

但是对这个问题,很多情况下,我们并不能清晰的把控处理程序所消耗的时长,为了能够按照一定的间隔周期性的触发定时器,我们可以使用setTimeout来代替setInterval执行。

setTimeout(function fn(){
  // todo
  setTimeout(fn,10)
    // 执行完处理程序的内容后,在末尾再间隔10毫秒来调用该程序,这样就能保证一定是10毫秒的周期调用,这里时间按自己的需求来写
},10)

解决方案

方法一:requestAnimationFrame

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,理想状态下回调函数执行次数通常是每秒60次(也就是我们所说的60fsp),也就是每16.7ms 执行一次,但是并不一定保证为 16.7 ms。

const t = Date.now()
function mySetTimeout (cb, delay) {
  let startTime = Date.now()
  loop()
  function loop () {
    if (Date.now() - startTime >= delay) {
      cb();
      return;
    }
    requestAnimationFrame(loop)
  }
}
mySetTimeout(()=>console.log('mySetTimeout' ,Date.now()-t),2000) //2005
setTimeout(()=>console.log('SetTimeout' ,Date.now()-t),2000) // 2002

这种方案看起来像是增加了误差,这是因为requestAnimationFrame每16.7ms 执行一次,因此它不适用于间隔很小的定时器修正。

方法二: Web Worker

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXMLchannel属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序(反之亦然)。

Web Worker 的作用就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程不会被阻塞或拖慢。

// index.js
let count = 0;
//耗时任务
setInterval(function(){
  let i = 0;
  while(i++ < 100000000);
}, 0);

// worker
let worker = new Worker('./worker.js')
// worker.js
let startTime = new Date().getTime();
let count = 0;
setInterval(function(){
    count++;
    console.log(count + ' --- ' + (new Date().getTime() - (startTime + count * 1000)));
}, 1000);

这种方案体验整体上来说还是比较好的,既能较大程度修正计时器也不影响主进程任务

总结

由于js的单线程特性,所以会有事件排队、先进先出、setInterval调用被废弃、定时器无法保证准时执行回调函数以及出现setInterval的连续执行。

到此这篇关于JS定时器不可靠的原因及解决方案的文章就介绍到这了,更多相关JS定时器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaScript定时器类型总结

    目录 1.setInterval() 2.setTimeout() 1.setInterval() 按照指定的周期(以毫秒计)来调用函数. 方法会不停地调用函数,直到调用clearInterval() 或窗口被关闭. 语法: setInterval(code,millisec,[arg1, arg2, ...]) 参数 描述 code 必需.要执行的代码串. millisec 必须.时间间隔,以毫秒计. arg1, arg2, - 可选.当定时器过期的时候,将被传递给func指定函数的附加参数

  • JavaScript 定时器详情

    目录 1.简单介绍 2.setInterval 2.1 描述 2.2 参数 2.3 返回值 2.4 用法 3.setTimeout 3.1 描述 3.2 参数 3.3 用法 4.取消timer 5.在控制台使用定时器 1.简单介绍 在JavaScript中定时器有两个 setInterval() 与 setTimeout() 分别还有取消定时器的方法. 这都是window的对象,调用时可以省略window.这两个方法并不在 JavaScript 的规范中. 定时器方法相关方法有四种: 方法 描述

  • JavaScript定时器实现限时秒杀功能

    本文实例为大家分享了JavaScript实现限时秒杀功能的具体代码,供大家参考,具体内容如下 文件index.html 代码: <!DOCTYPE html> <html> <head> <meta charset="GBK" /> <title>show</title> <link rel="stylesheet" href="css/index.css" type=

  • JavaScript定时器原理

    目录 一. setTimeout() 定时器 二.停止 setTimeout() 定时器 三.setInterval() 定时器 四.清除setInterval() 定时器 五.电子时钟案例 前言: 在很多页面中,我们都可以看到一些倒计时或者和时间相关的效果,今天小熊将就JavaScript里面的倒计时做一概述. 首先,我们先来看看定时器,在JS中,有两种定时器: 一. setTimeout() 定时器 语法: window.setTimeout(调用函数, [延迟的毫秒数]); setTime

  • 详解JavaScript的定时器

    目录 简单介绍 setInterval 描述 参数 返回值 用法 setTimeout 描述 参数 用法: 取消timer 在控制台使用定时器 console.time(timerName) console.timeEnd(timerName) 用法 总结 简单介绍 在JavaScript中定时器有两个 setInterval() 与 setTimeout() 分别还有取消定时器的方法. 这都是window的对象,调用时可以省略window.这两个方法并不在 JavaScript 的规范中. 定

  • JS定时器不可靠的原因及解决方案

    目录 前言 定时器工作原理 setInterval调用被废弃 事件循环 导致定时器不可靠的原因 延迟执行时间有最大值 解决方案 总结 前言 在工作中应用定时器的场景非常多,但你会发现有时候定时器好像并没有按照我们的预期去执行,比如我们常遇到的setTimeout(()=>{},0)它有时候并不是按我们预期的立马就执行.想要知道为什么会这样,我们首先需要了解Javascript计时器的工作原理. 定时器工作原理 为了理解计时器的内部工作原理,我们首先需要了解一个非常重要的概念:计时器设定的延时是没

  • js定时器出现第一次延迟的原因及解决方法

    我们在使用js定时器,经常会出现间隔几秒获取一次数据,这是通过setInterval实现的.而且如果setInterval() 参数传递不当,定时器会延迟试行.本文向大家介绍js定时器第一次延迟的原理及实现过程. setInterval() 作用是在播放动画的时,每隔一定时间就调用函数.方法或对象. 语法 setInterval(function(),time); 单位是毫秒 注意:单位是毫秒 定时器第一次延迟执行:采用setInterval实现 var t = setInterval(scro

  • 初学vue出现空格警告的原因及其解决方案

    初学vue自己新建一个vue项目来做学习demo.不过在编写代码时一直出现空格不规范的警告.严重影响初学者的热情.错误如下图所示.(这样的错误很多,但大概翻译成中文的意思都是说空格使用不规范.) 这是因为你使用 ESLint,用来规范代码风格的.你的 Webpack 配置中大概是使用了 eslint-loader.在多人协作或大项目中推荐使用,不想要就在 webpack.config.js 中去掉.eslint是语法检查工具,但限制太过于严格,大部分开发人员无法适应,所以产生这个需求. 解决办法

  • vue 动态添加的路由页面刷新时失效的原因及解决方案

    目录 问题描述 场景 接口返回 前端菜单定义 vuex中的方法 出现的问题 原因分析 解决方案 实现代码 route/index.js的导航守卫中添加逻辑判断 总结 问题描述 昨天在做vue后台管理系统有关权限页面动态添加到路由的功能时,遇到一个问题:动态添加的路由页面,在页面刷新时出现了404的情况. 场景 后台管理系统的权限控制是通过在前端页面定义权限code, 把code给后台同学保存配置到表中,之后根据后台返回的权限code列表与前端页面配置的code菜单列表做筛选匹配,code相等的页

  • ajax请求前端跨域问题原因及解决方案

    目录 一.跨域是怎么形成的 二.导致跨域的根本原因 三.解决方法 1 .JSONP 2. CORS 3 .代理转发 一.跨域是怎么形成的 当我们请求一个url的 协议.域名.端口三者之间任意一个与当前页面url的协议.域名.端口 不同这种现象我们把它称之为跨域 跨域会导致: 1.无法读取非同源网页的 Cookie.LocalStorage 和 IndexedDB 2.无法接触非同源网页的 DOM 3.无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应) 二.导致跨域的根本原因

  • 浏览器切换到其他标签页或最小化js定时器是否准时测试

    目录 前言 浏览器可见和不可见状态 setInterval setTimeout requestAnimationFrame 总结 如何解决 前言 这是我最近开发碰到的一个问题,本文是我测试出来的实践结果,供大家参考. 关于js定时器,setInterval和setTimeout,作为我们日常开发经常使用到的方法,大家一定非常熟悉.比如下面一个例子: setInterval(() => { console.log('1'); }, 500); 作为刚学前端没多久的新人也能知道,这段代码就是每过5

  • js定时器实现倒计时效果

    本文实例为大家分享了js定时器实现倒计时效果展示的具体代码,供大家参考,具体内容如下 日期函数 倒计时 =  用 将来的时间  -   现在的时间 问题:将来时间 距离 1970 毫秒数   -     现在距离 1970年1 用将来的毫秒数 -  现在的毫秒数   不断转换就可以了 <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <t

  • JS定时器实现数值从0到10来回变化

    效果:数值从0到10来回变化 代码: var a=0 var timer1,timer2; function add(){ a++; console.log(a); if(a>=10){ clearInterval(timer1); timer2=setInterval(sub,200); } } function sub(){ a--; console.log(a); if(a<=0){ clearInterval(timer2); timer1=setInterval(add,200);

  • js定时器的使用(实例讲解)

    在javascritp中,有两个关于定时器的专用函数,分别为: 1.倒计定时器:timename=setTimeout("function();",delaytime);2.循环定时器:timename=setInterval("function();",delaytime); 第一个参数"function()"是定时器触发时要执行的动作,可以是一个函数,也可以是几个函数,函数间用":"隔开即可.比如要弹出两个警告窗口,便可将

  • NODE.JS跨域问题的完美解决方案

    这几天公司同事(前端)写页面的时候一直说拿不到想要的JSON,安卓iOS那边是可以拿到的,但他也是新手也不知道为什么只知道是js跨域问题,然后问我我也不懂前端我开始百度, 有人说是谷歌浏览器跨域要设置一下,然后我就在谷歌浏览器的目标后面加一个 --disable-web-security 但是后来发现依然报错,依然拿不到想要的数据.后来也不停的找找找也没有什么眉目. 直到今天百度了一下PHP的跨域启发了我,于是百度找到了node.js的跨域问题,最后我在 app.js 路由设置里面加了一段跨域代

随机推荐