提问人:pimvdb 提问时间:5/8/2011 最后编辑:Giacomo1968pimvdb 更新时间:7/30/2023 访问量:212224
当标签页在 Chrome 中处于非活动状态时,如何使 setInterval 也能正常工作?
How can I make setInterval also work when a tab is inactive in Chrome?
问:
我每秒运行一段代码 30 次。这很好用,但是当我选择另一个选项卡(以便包含我的代码的选项卡变为非活动状态)时,由于某种原因将其设置为空闲状态。setInterval
setInterval
我做了这个简化的测试用例(http://jsfiddle.net/7f6DX/3/):
var $div = $('div');
var a = 0;
setInterval(function() {
a++;
$div.css("left", a)
}, 1000 / 30);
如果运行此代码,然后切换到另一个选项卡,请等待几秒钟并返回,动画将继续在切换到另一个选项卡时的位置。
因此,如果选项卡处于非活动状态,动画不会每秒运行 30 次。这可以通过计算每秒调用函数的次数来确认 - 如果选项卡处于非活动状态,则不会是 30 次,而只是 1 或 2 次。setInterval
我想这是为了提高系统性能而设计完成的,但是有什么方法可以禁用此行为吗?
在我的场景中,这实际上是一个缺点。
答:
在大多数浏览器上,非活动选项卡的执行优先级较低,这可能会影响 JavaScript 计时器。
如果转换值是使用帧之间经过的实时时间而不是每个间隔的固定增量计算的,则您不仅可以解决此问题,还可以使用 requestAnimationFrame 实现窒息动画,因为如果处理器不是很忙,它可以达到 60fps。
下面是一个普通的 JavaScript 示例,使用:requestAnimationFrame
var target = document.querySelector('div#target')
var startedAt, duration = 3000
var domain = [-100, window.innerWidth]
var range = domain[1] - domain[0]
function start() {
startedAt = Date.now()
updateTarget(0)
requestAnimationFrame(update)
}
function update() {
let elapsedTime = Date.now() - startedAt
// playback is a value between 0 and 1
// being 0 the start of the animation and 1 its end
let playback = elapsedTime / duration
updateTarget(playback)
if (playback > 0 && playback < 1) {
// Queue the next frame
requestAnimationFrame(update)
} else {
// Wait for a while and restart the animation
setTimeout(start, duration/10)
}
}
function updateTarget(playback) {
// Uncomment the line below to reverse the animation
// playback = 1 - playback
// Update the target properties based on the playback position
let position = domain[0] + (playback * range)
target.style.left = position + 'px'
target.style.top = position + 'px'
target.style.transform = 'scale(' + playback * 3 + ')'
}
start()
body {
overflow: hidden;
}
div {
position: absolute;
white-space: nowrap;
}
<div id="target">...HERE WE GO</div>
对于后台任务(与 UI 无关)
@UpTheCreek评论:
对于演示问题很好,但仍然 有些事情你需要继续运行。
如果您有需要以给定时间间隔精确执行的后台任务,则可以使用 HTML5 Web Worker。看看下面的 Möhre 的回答,了解更多详情......
CSS 与 JS“动画”
这个问题和许多其他问题可以通过使用 CSS 过渡/动画而不是基于 JavaScript 的动画来避免,这会增加相当大的开销。我推荐这个jQuery插件,让你从CSS转换中受益,就像方法一样。animate()
评论
before = now;
elapsedTime
只需这样做:
var $div = $('div');
var a = 0;
setInterval(function() {
a++;
$div.stop(true,true).css("left", a);
}, 1000 / 30);
非活动的浏览器选项卡缓冲某些 or 函数。setInterval
setTimeout
stop(true,true)
将停止所有缓冲事件,并立即仅执行最后一个动画。
该方法现在会强制要求在非活动选项卡中每秒发送不超过一个超时。此外,它现在将嵌套超时限制到 HTML5 规范允许的最小值:4 毫秒(而不是过去限制的 10 毫秒)。window.setTimeout()
评论
我认为对这个问题的最佳理解是在这个例子中:http://jsfiddle.net/TAHDb/
我在这里做一件简单的事情:
间隔 1 秒,每次隐藏第一个跨度并将其移动到最后一个跨度,并显示第二个跨度。
如果您停留在页面上,它将按预期工作。 但是,如果你把标签隐藏几秒钟,当你回来时,你会看到一个奇怪的东西。
就像在您现在不活跃期间没有发生的所有事件一样,将在 1 次内全部发生。所以在几秒钟内,你会得到 X 事件。它们速度如此之快,以至于可以一次看到所有 6 个跨度。
所以它接缝 chrome 只会延迟事件,所以当你回来时,所有事件都会发生,但都是一次性发生的......
这是一个实用的应用程序,如果发生这种情况,那就是一个简单的幻灯片。想象一下数字是图像,如果用户在回来时保持隐藏选项卡,他将看到所有图像都漂浮在空中,完全迷住了。
要解决此问题,请使用 stop(true,true),就像 pimvdb 告诉的那样。 这将清除事件队列。
评论
requestAnimationFrame
我在音频衰落和 HTML5 播放器方面遇到了同样的问题。当选项卡变为非活动状态时,它被卡住了。 因此,我发现 WebWorker 可以不受限制地使用间隔/超时。我用它来将“刻度”发布到主 javascript。
WebWorkers 代码:
var fading = false;
var interval;
self.addEventListener('message', function(e){
switch (e.data) {
case 'start':
if (!fading){
fading = true;
interval = setInterval(function(){
self.postMessage('tick');
}, 50);
}
break;
case 'stop':
clearInterval(interval);
fading = false;
break;
};
}, false);
主要 Javascript:
var player = new Audio();
player.fader = new Worker('js/fader.js');
player.faderPosition = 0.0;
player.faderTargetVolume = 1.0;
player.faderCallback = function(){};
player.fadeTo = function(volume, func){
console.log('fadeTo called');
if (func) this.faderCallback = func;
this.faderTargetVolume = volume;
this.fader.postMessage('start');
}
player.fader.addEventListener('message', function(e){
console.log('fader tick');
if (player.faderTargetVolume > player.volume){
player.faderPosition -= 0.02;
} else {
player.faderPosition += 0.02;
}
var newVolume = Math.pow(player.faderPosition - 1, 2);
if (newVolume > 0.999){
player.volume = newVolume = 1.0;
player.fader.postMessage('stop');
player.faderCallback();
} else if (newVolume < 0.001) {
player.volume = newVolume = 0.0;
player.fader.postMessage('stop');
player.faderCallback();
} else {
player.volume = newVolume;
}
});
评论
有一个解决方案可以使用Web Worker(如前所述),因为它们在单独的进程中运行并且不会减慢速度
我编写了一个很小的脚本,可以在不更改代码的情况下使用 - 它只是覆盖函数 setTimeout、clearTimeout、setInterval、clearInterval。
只需将其包含在所有代码之前即可。
评论
这是我的粗略解决方案
(function(){
var index = 1;
var intervals = {},
timeouts = {};
function postMessageHandler(e) {
window.postMessage('', "*");
var now = new Date().getTime();
sysFunc._each.call(timeouts, function(ind, obj) {
var targetTime = obj[1];
if (now >= targetTime) {
obj[0]();
delete timeouts[ind];
}
});
sysFunc._each.call(intervals, function(ind, obj) {
var startTime = obj[1];
var func = obj[0];
var ms = obj[2];
if (now >= startTime + ms) {
func();
obj[1] = new Date().getTime();
}
});
}
window.addEventListener("message", postMessageHandler, true);
window.postMessage('', "*");
function _setTimeout(func, ms) {
timeouts[index] = [func, new Date().getTime() + ms];
return index++;
}
function _setInterval(func, ms) {
intervals[index] = [func, new Date().getTime(), ms];
return index++;
}
function _clearInterval(ind) {
if (intervals[ind]) {
delete intervals[ind]
}
}
function _clearTimeout(ind) {
if (timeouts[ind]) {
delete timeouts[ind]
}
}
var intervalIndex = _setInterval(function() {
console.log('every 100ms');
}, 100);
_setTimeout(function() {
console.log('run after 200ms');
}, 200);
_setTimeout(function() {
console.log('closing the one that\'s 100ms');
_clearInterval(intervalIndex)
}, 2000);
window._setTimeout = _setTimeout;
window._setInterval = _setInterval;
window._clearTimeout = _clearTimeout;
window._clearInterval = _clearInterval;
})();
评论
postMessageHandler
我能够使用音频标签并在至少 250 毫秒内调用我的回调函数并处理其 ontimeupdate 事件。它在一秒钟内调用了 3-4 次。它比一秒滞后 setTimeout 要好
深受鲁斯兰·图绍夫(Ruslan Tushov)图书馆的影响,我创建了自己的小型图书馆。只需将脚本添加到 中,它就会修补并使用 .<head>
setInterval
setTimeout
WebWorker
评论
播放音频文件可暂时确保完整的后台 Javascript 性能
对我来说,这是最简单、侵入性最小的解决方案——除了播放微弱/几乎空的声音外,没有其他潜在的副作用
您可以在此处找到详细信息:https://stackoverflow.com/a/51191818/914546
(从其他答案中,我看到有些人使用Audio标签的不同属性,我确实想知道是否可以使用Audio标签来获得完整的性能,而无需实际播放某些内容)
对我来说,像这里的其他人一样在后台播放音频并不重要,我的问题是我有一些动画,当你在其他选项卡中并返回它们时,它们表现得很疯狂。我的解决方案是将这些动画放入其中,如果这可以防止非活动选项卡:
if (!document.hidden){ //your animation code here }
多亏了这一点,我的动画仅在选项卡处于活动状态时才运行。 我希望这能帮助某人处理我的案子。
评论
当选项卡处于非活动状态或工作但在正确的时间段不工作时,两者都不起作用。解决方案是将另一个源用于时间事件。例如,Web 套接字或 Web 辅助角色是两个事件源,当 Tab 处于非活动状态时,它们可以正常工作。因此,无需将所有代码移动到 Web Worker,只需使用 Worker 作为时间事件源即可:setInterval
requestAnimationFrame
// worker.js
setInterval(function() {
postMessage('');
}, 1000 / 50);
.
var worker = new Worker('worker.js');
var t1 = 0;
worker.onmessage = function() {
var t2 = new Date().getTime();
console.log('fps =', 1000 / (t2 - t1) | 0);
t1 = t2;
}
此示例的 jsfiddle 链接。
评论
注意:如果您喜欢在后台工作的间隔,例如,播放音频或其他内容,则此解决方案不适用。但是,如果您对动画在返回页面或选项卡时无法正常工作感到困惑,这是一个很好的解决方案。
有很多方法可以实现这个目标,也许“WebWorkers”是最标准的,但可以肯定的是,它不是最简单和方便的,特别是如果你没有足够的时间,所以你可以尝试这种方式:
基本概念:
为您的间隔(或动画)建立一个名称并设置您的间隔(动画),以便在用户第一次打开您的页面时运行:
var interval_id = setInterval(your_func, 3000);
每次浏览器(选项卡)被停用并重新运行您的间隔(动画)时,您都可以再次通过
$(window).focus(function() {});
$(window).blur(function() {});
clearInterval(interval_id)
interval_id = setInterval();
示例代码:
var interval_id = setInterval(your_func, 3000);
$(window).focus(function() {
interval_id = setInterval(your_func, 3000);
});
$(window).blur(function() {
clearInterval(interval_id);
interval_id = 0;
});
这是一个很古老的问题,但我遇到了同样的问题。
如果您在 chrome 上运行您的网络,您可以通读这篇文章 Chrome 57 中的背景选项卡。
基本上,如果间隔计时器没有用完计时器预算,则可以运行。
预算的消耗基于计时器内任务的 CPU 时间使用情况。
根据我的场景,我将视频绘制到画布并传输到 WebRTC。
即使选项卡处于非活动状态,webrtc 视频连接也会不断更新。
但是,您必须使用 instead 而不是 但是,不建议将 itt 用于 UI 渲染。setInterval
requestAnimationFrame
最好听事件并相应地改变呈现机械主义。visibilityChange
此外,您可以尝试 Kaan Soral 的建议,它应该可以根据文档工作。
评论
我在这里为任何试图在计时器函数中解决这个问题的人带来了一个简单的解决方案,正如@kbtzr在另一个答案中提到的,我们可以使用 Date 对象而不是固定增量来计算自开始以来经过的时间,即使您不在应用程序的选项卡中,这也将起作用。
这是示例 HTML。
<body>
<p id="time"></p>
</body>
然后这个 JavaScript:
let display = document.querySelector('#time')
let interval
let time
function startTimer() {
let initialTime = new Date().getTime()
interval = setInterval(() => {
let now = new Date().getTime()
time = (now - initialTime) + 10
display.innerText = `${Math.floor((time / (60 * 1000)) % 60)}:${Math.floor((time / 1000) % 60)}:${Math.floor((time / 10) % 100)}`
}, 10)
}
startTimer()
这样,即使由于非活动选项卡的性能原因而增加了间隔值,所做的计算也将保证正确的时间。这是一个普通的代码,但我在我的 React 应用程序中使用了这个逻辑,你也可以根据需要修改它。
此问题有一个解决方法,尽管实际上必须在某些窗口中使选项卡处于活动状态。
- 将非活动标签页设为单独的浏览器窗口。
- 不要使任何其他窗口最大化(除非最大化的窗口在您的后面)。
这应该给浏览器一种始终处于活动状态的印象。
这有点麻烦,但也是一个快速的胜利。前提是可以控制窗户的布置。
评论
我通过添加一个正常运行的 UI 修改了 Lacerda 的响应。
我添加了开始/恢复/暂停/停止操作。
const
timer = document.querySelector('.timer'),
timerDisplay = timer.querySelector('.timer-display'),
toggleAction = timer.querySelector('[data-action="toggle"]'),
stopAction = timer.querySelector('[data-action="stop"]'),
tickRate = 10;
let intervalId, initialTime, pauseTime = 0;
const now = () => new Date().getTime();
const formatTime = (hours, minutes, seconds) =>
[hours, minutes, seconds]
.map(v => `${isNaN(v) ? 0 : v}`.padStart(2, '0'))
.join(':');
const update = () => {
let
time = (now() - initialTime) + 10,
hours = Math.floor((time / (60000)) % 60),
minutes = Math.floor((time / 1000) % 60),
seconds = Math.floor((time / 10) % 100);
timerDisplay.textContent = formatTime(hours, minutes, seconds);
};
const
startTimer = () => {
initialTime = now();
intervalId = setInterval(update, tickRate);
},
resumeTimer = () => {
initialTime = now() - (pauseTime - initialTime);
intervalId = setInterval(update, tickRate);
},
pauseTimer = () => {
clearInterval(intervalId);
intervalId = null;
pauseTime = now();
},
stopTimer = () => {
clearInterval(intervalId);
intervalId = null;
initialTime = undefined;
pauseTime = 0;
},
restartTimer = () => {
stopTimer();
startTimer();
};
const setButtonState = (button, state, text) => {
button.dataset.state = state;
button.textContent = text;
};
const
handleToggle = (e) => {
switch (e.target.dataset.state) {
case 'pause':
setButtonState(e.target, 'resume', 'Resume');
pauseTimer();
break;
case 'resume':
setButtonState(e.target, 'pause', 'Pause');
resumeTimer();
break;
default:
setButtonState(e.target, 'pause', 'Pause');
restartTimer();
}
},
handleStop = (e) => {
stopTimer();
update();
setButtonState(toggleAction, 'initial', 'Start');
};
toggleAction.addEventListener('click', handleToggle);
stopAction.addEventListener('click', handleStop);
update();
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
background: #000;
}
.timer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.5em;
}
.timer .timer-display {
font-family: monospace;
font-size: 3em;
background: #111;
color: #8F8;
border: thin solid #222;
padding: 0.25em;
}
.timer .timer-actions {
display: flex;
justify-content: center;
gap: 0.5em;
}
.timer .timer-actions button[data-action] {
font-family: monospace;
width: 6em;
border: thin solid #444;
background: #222;
color: #EEE;
padding: 0.5em;
cursor: pointer;
text-transform: uppercase;
}
.timer .timer-actions button[data-action]:hover {
background: #444;
border: thin solid #666;
color: #FFF;
}
<div class="timer">
<div class="timer-display"></div>
<div class="timer-actions">
<button data-action="toggle" data-state="initial">Start</button>
<button data-action="stop">Stop</button>
</div>
</div>
受 Ruslan 回答的启发,一个方便的小类,您可以复制并粘贴到您的代码中,作为以下内容的替代品:setInterval
class WorkerInterval {
worker = null;
constructor(callback, interval) {
const blob = new Blob([`setInterval(() => postMessage(0), ${interval});`]);
const workerScript = URL.createObjectURL(blob);
this.worker = new Worker(workerScript);
this.worker.onmessage = callback;
}
stop() {
this.worker.terminate();
}
}
用法示例:
const interval = new WorkerInterval(callback, 100);
// sometime later
interval.stop();
评论
Date
Date