Меня всегда беспокоила непредсказуемость метода setTimeout() в Javascript.
По моему опыту, во многих ситуациях таймер ужасно неточен. Под неточным я подразумеваю, что фактическое время задержки, кажется, варьируется на 250-500 мс более или менее. Хотя это не очень много времени, при его использовании для скрытия / отображения элементов пользовательского интерфейса время может быть заметно заметно.
Есть ли какие-то уловки, которые можно сделать, чтобы убедиться, что setTimeout() работает точно (без использования внешнего API), или это безнадежное дело?



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Ненавижу это говорить, но я не думаю, что есть способ облегчить это. Я действительно думаю, что это зависит от клиентской системы, поэтому более быстрый движок или машина javascript могут сделать немного более точным.
По моему опыту, это потеря усилий, даже несмотря на то, что наименьшее разумное время, в течение которого, как я когда-либо видел, действует js, составляет около 32-33 мс. ...
Здесь определенно есть ограничение. Чтобы дать вам некоторую перспективу, только что выпущенный Google браузер Chrome достаточно быстр, чтобы выполнить setTimeout (function () {}, 0) за 15–20 мс, тогда как старые движки Javascript выполняли эту функцию за сотни миллисекунд. Хотя setTimeout использует миллисекунды, ни одна виртуальная машина javascript на данный момент не может выполнять код с такой точностью.
Я слышал, что Chrome - первый браузер с такой точностью, но мне до сих пор не удалось это проверить.
Вы немного ошиблись: 15 мсек - это стандартная гранулярность таймера в Windows, и, следовательно, гранулярность для таймеров в большинстве браузеров Бег в Windows. Очевидно, Chrome использует более точные аппаратные таймеры для большей детализации. Отдельная проблема - скорость выполнения JS.
У Chrome есть ошибки для этого - увеличение разрешения setTimeout нарушает работу некоторых веб-сайтов, которые принимают разрешение 15 мс, и разрушает время автономной работы в Windows (архитектура Windows, по-видимому, борется с таймерами с приличным разрешением)
Дэн, исходя из моего опыта (который включает в себя реализацию языка SMIL2.1 в JavaScript, где управление временем является предметом обсуждения), я могу заверить вас, что вам никогда не понадобится высокая точность setTimeout или setInterval.
Однако имеет значение порядок, в котором setTimeout / setInterval выполняется при постановке в очередь - и это всегда отлично работает.
Хотя мне никогда не НУЖНА была высокоточная синхронизация, может быть неприятно, когда задержка, которую я установил для чего-то, что зависит от пользовательского интерфейса, немного отключена. Ничего не ломается, но это можно заметить, потому что это непоследовательно.
Shog9 предоставил полное представление о том, что я завернул, с помощью простых утверждений. И да, это не «браузер» или «реализация javascript», которые не могут гарантировать время, это ОС, люди, мир и т. д. Таким образом, скорее полагайтесь на «происходящее», чем на «время».
Are there any tricks that can be done to ensure that
setTimeout()performs accurately (without resorting to an external API) or is this a lost cause?
Нет и нет. С setTimeout() вы не приблизитесь к идеально точному таймеру - браузеры для этого не настроены. тем не мение, вам также не нужно полагаться на него для определения времени. Большинство анимационных библиотек поняли это много лет назад: вы настраиваете обратный вызов с помощью setTimeout(), но определяете, что нужно сделать, на основе значения (new Date()).milliseconds (или эквивалентного). Это позволяет использовать более надежную поддержку таймера в новых браузерах, но при этом вести себя соответствующим образом в старых браузерах.
Это также позволяет избегайте использования слишком большого количества таймеров! Это важно: каждый таймер - это обратный вызов. Каждый обратный вызов выполняет код JS. Во время выполнения JS-кода события браузера, включая другие обратные вызовы, задерживаются или отбрасываются. Когда обратный вызов завершается, дополнительные обратные вызовы должны конкурировать с другими событиями браузера для возможности выполнения. Следовательно, один таймер, который обрабатывает все ожидающие задачи для этого интервала, будет работать лучше, чем два таймера с совпадающими интервалами, и (для коротких тайм-аутов) лучше, чем два таймера с перекрывающимися тайм-аутами!
Резюме: прекратите использование setTimeout() для реализации проектов «один таймер / одна задача» и используйте часы реального времени для сглаживания действий пользовательского интерфейса.
Как насчет вызова setTimeout () из рабочего. Разве это не уменьшит блокировку, вызванную рендерингом?
Нет никаких гарантий, @hidarikani. FWIW, это может быть такой же проблемой в собственном коде, и в браузере у вас нет возможности требовать таймеры с высоким разрешением и задержку в реальном времени. Не то чтобы вам хотелось; это рецепт напрасной траты ЦП, и вы должны предположить, что, по крайней мере, в некоторых случаях браузер и ОС будут делать все возможное, чтобы продлить срок службы батареи и жертвовать точностью вашего таймера для этой цели.
Я думаю, что этот ответ на самом деле неверен, так как мы получили requestAnimationFrame ... См. Также этот ответ от Крис. Это не такое уж простое решение, но оно удовлетворяет критериям OP отсутствия внешнего API.
requestAnimationFrame предлагает больший точность, но не обязательно лучшую точность @StijndeWitt - вы все еще зависите от браузера, который может быть остановлен другой обработкой или даже любым выбором, который вы сделаете при реализации в обратном вызове. Техника Криса хороша, но если вы хотите, чтобы логика выполнялась как можно ближе к установленному времени, вам все равно нужно решить, что нужно делать, на основе значения извлеченного времени; requestAnimationFrame предлагает даже меньшую гарантию того, когда он будет вызван, чем setTimeout. Также см. Мою предыдущую заметку к хидарикани.
«requestAnimationFrame предлагает еще меньшую гарантию» Это очень хороший момент! Спасибо
Тайм-ауты JavaScript имеют ограничение по умолчанию в 10-15 мс (я не уверен, что вы делаете, чтобы получить 200 мс, если только вы не выполняете 185 мс фактического выполнения js). Это связано с тем, что окна имеют стандартное разрешение таймера 15 мс, единственный способ добиться большего - использовать таймеры с более высоким разрешением Windows, которые являются общесистемными настройками, поэтому могут работать с другими приложениями в системе, а также расходуют время автономной работы (Chrome имеет ошибка от Intel по этому вопросу).
Стандарт де-факто 10-15 мс обусловлен тем, что люди используют таймаут 0 мс на веб-сайтах, но затем кодируют таким образом, что предполагает тайм-аут 10-15 мс (например, js-игры, которые предполагают 60 кадров в секунду, но запрашивают 0 мс / кадр без дельта-логики, поэтому игра / сайт / анимация идет на несколько порядков быстрее, чем предполагалось). Чтобы учесть это, даже на платформах, у которых нет проблем с таймером Windows, браузеры ограничивают разрешение таймера 10 мс.
Чтобы быть ясным, Chrome на самом деле учитывает тайм-ауты 0 мс (скорее, выполняет их в самый непосредственный момент).
Ответ shog9 в значительной степени соответствует тому, что я бы сказал, хотя я бы добавил следующее об анимации / событиях пользовательского интерфейса:
Если у вас есть поле, которое должно скользить по экрану, расширяться вниз, а затем исчезать в его содержимом, не пытайтесь разделить все три события с задержками, рассчитанными так, чтобы они запускались одно за другим - используйте обратные вызовы, поэтому один раз первое событие выполнено скольжением, оно вызывает расширитель, как только это сделано, он вызывает фейдер. jQuery может сделать это легко, и я уверен, что другие библиотеки тоже.
Вам нужно «подкрасться» к установленному времени. Некоторые методы проб и ошибок будут необходимы, но по сути.
Установите тайм-аут для завершения около 100 мс до требуемого времени
сделайте функцию обработчика тайм-аута следующим образом:
calculate_remaining_time
if remaining_time > 20ms // maybe as much as 50
re-queue the handler for 10ms time
else
{
while( remaining_time > 0 ) calculate_remaining_time;
do_your_thing();
re-queue the handler for 100ms before the next required time
}
Но ваш цикл while все еще может быть прерван другими процессами, поэтому он все еще не идеален.
Это неплохая идея, если предположить, что вам нужно установить точное время на настенных часах вовремя. Но если вы сразу после согласованного интервала, вы можете добиться аналогичных результатов, просто изменив значение тайм-аута на лету до чего-то, что немного меньше периода интервала, за вычетом времени, затрачиваемого на вашу обработку. И даже для чего-то вроде напоминания или тикающих часов в большинстве случаев, вероятно, будет достаточно точности в 15 мс.
Единственный раз, когда мне понадобилась такая точность, - это попытаться воспроизвести музыкальный бит. Но таймеры Javascript не совсем подходят для этой работы. Даже при таком дизайне тиканье довольно неустойчивое и не совсем подходит для метронома.
Если вам нужно получить точный обратный вызов на заданном интервале, эта суть может вам помочь:
https://gist.github.com/1185904
function interval(duration, fn){
var _this = this
this.baseline = undefined
this.run = function(){
if (_this.baseline === undefined){
_this.baseline = new Date().getTime()
}
fn()
var end = new Date().getTime()
_this.baseline += duration
var nextTick = duration - (end - _this.baseline)
if (nextTick<0){
nextTick = 0
}
_this.timer = setTimeout(function(){
_this.run(end)
}, nextTick)
}
this.stop = function(){
clearTimeout(_this.timer)
}
}
.
REF; http://www.sitepoint.com/creating-accurate-timers-in-javascript/
Этот сайт очень сильно выручил меня.
Вы можете использовать системные часы, чтобы компенсировать неточность таймера. Если вы запускаете функцию времени как серию вызовов setTimeout - каждый экземпляр вызывает следующий - тогда все, что вам нужно сделать, чтобы она была точной, - это точно определить, насколько она неточна, и вычесть эту разницу из следующей итерации:
var start = new Date().getTime(),
time = 0,
elapsed = '0.0';
function instance()
{
time += 100;
elapsed = Math.floor(time / 100) / 10;
if (Math.round(elapsed) == elapsed) { elapsed += '.0'; }
document.title = elapsed;
var diff = (new Date().getTime() - start) - time;
window.setTimeout(instance, (100 - diff));
}
window.setTimeout(instance, 100);
Этот метод минимизирует дрейф и уменьшит погрешности более чем на 90%.
Это устранило мои проблемы, надеюсь, это поможет
@Mr_Chimp, зачем тебе в коде переменная elapsed?
@AntonRudeshko Ха ... хороший вопрос. Мой код был адаптирован по ссылке выше. У меня считать это просто бесполезный остаток.
Функция doTimer в конце статьи - это именно то, что мне нужно. Это супер точно.
Вот пример демонстрации предложения Shog9. Это заполняет индикатор выполнения jquery плавно в течение 6 секунд, а затем перенаправляет на другую страницу после ее заполнения:
var TOTAL_SEC = 6;
var FRAMES_PER_SEC = 60;
var percent = 0;
var startTime = new Date().getTime();
setTimeout(updateProgress, 1000 / FRAMES_PER_SEC);
function updateProgress() {
var currentTime = new Date().getTime();
// 1000 to convert to milliseconds, and 100 to convert to percentage
percent = (currentTime - startTime) / (TOTAL_SEC * 1000) * 100;
$("#progressbar").progressbar({ value: percent });
if (percent >= 100) {
window.location = "newLocation.html";
} else {
setTimeout(updateProgress, 1000 / FRAMES_PER_SEC);
}
}
Откуда взялась функция setTimer ()?
@TimHallman Я имел в виду setTimeout(). Отредактирую соответственно.
Не так давно у меня была похожая проблема, и я предложил подход, сочетающий requestAnimationFrame с performance.now (), который работает очень эффективно.
Теперь я могу делать таймеры с точностью до 12 знаков после запятой:
window.performance = window.performance || {};
performance.now = (function() {
return performance.now ||
performance.mozNow ||
performance.msNow ||
performance.oNow ||
performance.webkitNow ||
function() {
//Doh! Crap browser!
return new Date().getTime();
};
})();
Если вы используете setTimeout(), чтобы быстро уступить браузеру, чтобы его поток пользовательского интерфейса мог справиться с любыми задачами, которые ему нужно выполнить (например, обновить вкладку или не отображать диалоговое окно Long Running Script), есть новый API под названием Эффективное создание скриптов, также известный как setImmediate(), который может работать немного лучше для тебя.
setImmediate() работает очень похоже на setTimeout(), но он может работать немедленно, если браузеру больше нечего делать. Во многих ситуациях, когда вы используете setTimeout(..., 16), setTimeout(..., 4) или setTimeout(..., 0) (т.е. вы хотите, чтобы браузер выполнял любые невыполненные задачи потока пользовательского интерфейса и не отображал диалоговое окно Long Running Script), вы можете просто заменить setTimeout() на setImmediate(), отбросив вторую (миллисекунду) аргумент.
Отличие от setImmediate() в том, что это в основном доходность; если браузеру нужно что-то сделать в потоке пользовательского интерфейса (например, обновить вкладку), он сделает это перед тем, как вернуться к вашему обратному вызову. Однако, если браузер уже полностью занят своей работой, обратный вызов, указанный в setImmediate(), будет выполняться без задержки.
К сожалению, в настоящее время он поддерживается только в IE9 +, поскольку есть некоторые отталкивать от других поставщиков браузеров.
Тем не менее, доступен хороший полифилл, если вы хотите его использовать и надеетесь, что другие браузеры в какой-то момент его реализуют.
Если вы используете setTimeout() для анимации, requestAnimationFrame - ваш лучший выбор, поскольку ваш код будет работать синхронно с частотой обновления монитора.
Если вы используете setTimeout() с более медленной частотой вращения педалей, например один раз каждые 300 миллисекунд вы можете использовать решение, подобное тому, что предлагает user1213320, где вы отслеживаете, сколько времени прошло с последней временной метки, запущенной вашим таймером, и компенсируйте любую задержку. Одним из улучшений является то, что вы можете использовать новый интерфейс Время высокого разрешения (он же window.performance.now()) вместо Date.now(), чтобы получить разрешение более миллисекунды для текущего времени.
Это таймер, который я сделал для своего музыкального проекта. Таймер, точный на всех устройствах.
var Timer = function(){
var framebuffer = 0,
var msSinceInitialized = 0,
var timer = this;
var timeAtLastInterval = new Date().getTime();
setInterval(function(){
var frametime = new Date().getTime();
var timeElapsed = frametime - timeAtLastInterval;
msSinceInitialized += timeElapsed;
timeAtLastInterval = frametime;
},1);
this.setInterval = function(callback,timeout,arguments) {
var timeStarted = msSinceInitialized;
var interval = setInterval(function(){
var totaltimepassed = msSinceInitialized - timeStarted;
if (totaltimepassed >= timeout) {
callback(arguments);
timeStarted = msSinceInitialized;
}
},1);
return interval;
}
}
var timer = new Timer();
timer.setInterval(function(){console.info("This timer will not drift."),1000}Оказывается, ваш код дрейфует примерно на 116 миллисекунд за 100 секунд: я добавил следующее перед вызовом timer.setInterval (): <code> var startTime = new Date (). GetTime (); </code> и добавил: <code> var сейчас = новая дата (). getTime (); var ms = (now - startTime) + 'ms'; </code> внутри обратного вызова, и результат был: Этот таймер БУДЕТ дрейфовать. мс от начала: 100116 мс
Вы можете рассмотреть возможность использования html5 webaudio часы, который использует системное время для большей точности.
Вот что я использую. Поскольку это JavaScript, я опубликую свои решения для Frontend и node.js:
Для обоих я использую одну и ту же функцию округления десятичных дробей, которую настоятельно рекомендую держать под рукой по следующим причинам:
const round = (places, number) => +(Math.round(number + `e+${places}`) + `e-${places}`)
places - Количество десятичных разрядов для округления, это должно быть безопасно и должно избегать проблем с числами с плавающей запятой (некоторые числа, такие как 1.0000000000005 ~, могут быть проблематичными). Я потратил время, исследуя лучший способ округления десятичных дробей, обеспечиваемый таймерами высокого разрешения, преобразованными в миллисекунды.
that + symbol - это унарный оператор, преобразующий операнд в число, практически идентичный Number().
Browser
const start = performance.now()
// I wonder how long this comment takes to parse
const end = performance.now()
const result = (end - start) + ' ms'
const adjusted = round(2, result) // see above rounding function
node.js
// Start timer
const startTimer = () => process.hrtime()
// End timer
const endTimer = (time) => {
const diff = process.hrtime(time)
const NS_PER_SEC = 1e9
const result = (diff[0] * NS_PER_SEC + diff[1])
const elapsed = Math.round((result * 0.0000010))
return elapsed
}
// This end timer converts the number from nanoseconds into milliseconds;
// you can find the nanosecond version if you need some seriously high-resolution timers.
const start = startTimer()
// I wonder how long this comment takes to parse
const end = endTimer(start)
console.info(end + ' ms')
У меня двухъядерный аппарат с частотой 2,8 ГГц, и таймеры все еще довольно неточные. По моему опыту, скорость процессора не имеет большого значения.