WebKit setInterval и изменение системного времени
При создании простой задачи я столкнулся со следующей проблемой: отображение html-часов с помощью движка WebKit. Дополнительным требованием было изменение системного времени и то, что оно должно работать в Windows. Я использовал setInterval для достижения этой цели, но кажется, что браузер зависает после того, как я изменил системное время в обратном направлении. Для меня это выглядит как вопрос WebKit. Воспроизвести в сафари легко, запустив этот простой код:
<p id="date"></p>
setInterval(SetTime, 1000);
function SetTime() {
document.getElementById('date').textContent=new Date();
}
После этого я сделал другой подход с рекурсивным вызовом setTimeout. Тот же эффект.
(function loop() {
document.getElementById('date').textContent=new Date();
setTimeout(loop, 1000);
})();
Есть идеи, почему это происходит и как обойти это?
1 ответ
Это почти наверняка проблема с WebKit.
Эта проблема
Когда вы используете setTimeout
Вы создаете "таймер" с двумя свойствами:
- Отметка времени, установленная сейчас + указанная задержка
- Обратный вызов срабатывает один раз, когда системное время больше, чем отметка времени.
Вы можете представить наивную реализацию setTimeout
выглядит примерно так:
var timers = [];
function setTimeout(callback, delay) {
var id = timers.length;
timers[id] = {
callback: callback,
timestamp: Date.now() + delay
}
return id;
}
Это просто создаст таймер и добавит его в список. Затем на каждом тике среда выполнения JS проверяет эти таймеры и выполняет обратные вызовы для тех, которые сработали:
var now = Date.now();
for(var id in timers) {
var timer = timers[id];
if(timer && timer.timestamp < now) {
callback();
delete timers[id];
}
}
Учитывая эту реализацию, представьте, что теперь меняется системное время (т.е. Date.now()
в примерах выше) к значению в прошлом - временная метка таймера будет по-прежнему установлена относительно предыдущего системного времени (то есть в будущем).
Та же проблема существует с setInterval
, который (при условии нормального кода в WebKit) будет реализован с использованием setTimeout
,
К сожалению, это означает, что любой вызов setTimeout
или же setInterval
будет страдать.
Решение
В качестве альтернативы вы можете использовать прекрасный window.requestAnimationFrame
метод для выполнения обратного вызова на каждом тике. Я вообще не проверял это, но он должен продолжать срабатывать при каждом тике, независимо от системного времени.
В качестве бонуса каждый раз, когда он запускает обратный вызов, вы получаете текущую метку времени в качестве параметра. Отслеживая предыдущую временную метку, переданную вашему обратному вызову, вы можете легко обнаружить обратные изменения системного времени:
var lastTimestamp;
var onNextFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
var checkForPast = function(timestamp) {
if(lastTimestamp && timestamp < lastTimestamp) {
console.error('System time moved into the past! I should probably handle this');
}
lastTimestamp = timestamp;
onNextFrame(checkForPast);
};
onNextFrame(checkForPast);
Выводы
Это может быть не очень хорошая новость для вас, но вам, вероятно, следует переписать все ваше приложение для использования requestAnimationFrame
в любом случае - кажется, что он больше подходит для ваших нужд:
var onNextFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
var dateElem = document.getElementById('date');
var updateDate = function(timestamp) {
dateElem.textContent = new Date();
onNextFrame(updateDate);
};
onNextFrame(updateDate);