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);
Другие вопросы по тегам