- Оригинальный адрес:A tour of JavaScript timers on the web
- Оригинальный автор:Nolan Lawson
- Перевод с:Команда облачных переводчиков Alibaba
- Ссылка на перевод:GitHub.com/рассветные команды/…
- Переводчик:Духовное болото
- Корректор:также дерево,Цзин Синь,облако сна
Тур по JavaScript-таймеру
Популярный тест: в чем разница между таймерами JavaScript?
- Promises
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- requestIdleCallback
Точнее, если вы отсортируете эти таймеры сразу, знаете ли вы, в каком порядке они срабатывают?
Если нет, то, возможно, вы не одиноки. Я много лет пишу и программирую на JavaScript, работаю на производителя браузеров более двух лет, и только недавно я по-настоящему понял эти таймеры и то, как их использовать.
В этом посте я дам общий обзор того, как работают эти таймеры и когда их использовать, а также расскажу, чем полезен Lodash.debounce()
а такжеthrottle()
функция.
Обещания и микрозадачи
Давайте начнем с этого, так как это, вероятно, самое простое. ОдинPromiseОбратные вызовы также известны как «микрозадачи», которые начинаются сMutationObserverОбратный вызов выполняется с той же частотой. еслиqueueMicrotask()Без исключения из спецификации и входа на территорию браузера это будет иметь тот же результат.
Я много писал об обещаниях. Однако стоит отметить, что промисы легко понять неправильно, потому что они не дают браузеру простоя. Это потому, что он находится в очереди асинхронного обратного вызова, но это не означает, что браузер может отображать или обрабатывать ввод или делать что-то еще, что мы хотим, чтобы браузер делал.
В качестве примера предположим, что у нас есть функция, которая блокирует основной поток на 1 секунду:
function block() {
var start = Date.now()
while (Date.now() - start < 1000) { /* wheee */ }
}
Если мы вызовем эту функцию с набором микрозадач:
for (var i = 0; i < 100; i++) {
Promise.resolve().then(block)
}
Это заблокирует браузер на 100 секунд. Это то же самое, что и следующая операция:
for (var i = 0; i < 100; i++) {
block()
}
Микрозадачи выполняются сразу после выполнения любых задач синхронизации. Между ними нет свободного времени для другой работы. Итак, если вы хотите разбить длительную задачу на микрозадачи, она не будет делать то, что вы хотите.
setTimeout и setInterval
Это два брата:setTimeoutзапланировать запуск задачи на X миллисекунд позже, в то время какsetIntervalЗапускать задачу каждые X миллисекунд.
Поскольку многие сайты, такие как конфетти, возятся сsetTimeout(0)
. Чтобы не блокировать основной поток браузера, браузер долженsetTimeout(/* ... */, 0)
Добавьте смягчения.
Этоcrashmybrowser.comпричины, по которым многие трюки вsetTimeout
Два других называются болееsetTimeout
изsetTimeout
и т.п. я здесь«Улучшение отклика ввода в Microsoft Edge»Некоторые из этих мер описаны в разделе «С периферии».
Говоря в широком смысле,setTimeout(0)
На самом деле не выполняется после 0 мс. Обычно выполняется в течение 4 мс. Иногда он выполняется менее чем за 16 мс (когда Edge заряжается). Иногда оно ограничено 1 секундой (пример:when running in a background tab). Это те возможности, которыми должны обладать браузеры, чтобы неконтролируемые веб-страницы не нагружали ЦП бесполезным выполнением.setTimeout
.
так,setTimeout
Позволяет браузеру выполнять некоторую работу (в отличие от микрозадач) перед вызовом обратного вызова. Однако, если вы хотите выполнять операции ввода или рендеринга перед обратным вызовом, как правило,setTimeout
Не лучший вариант, так как только иногда позволяет выполнять другие операции перед обратным вызовом. Теперь есть лучшие API для браузера, которые подключают к системе рендеринга браузера более напрямую.
setImmediate
Прежде чем перейти к использованию «лучших API-интерфейсов браузера», стоит упомянуть одну вещь. называетсяsetImmediateЗа неимением лучшего слова... странно. если вcaniuse.comПосмотрите, и вы обнаружите, что это поддерживают только браузеры Microsoft. ноОн также существует в node.js. Что за чертовщина?
setImmediate
Первоначально предложено Microsoft для решения вышеуказанныхsetTimeout
проблемы. в принципе,setTimeout
подвергся насилию,setImmediate(0)
фактическиsetImmediate(0)
, вместо того, что ограничено 4 мс. вы можете просмотретьsome discussion about it from Jason Weber back in 2011.
К сожалению,setImmediate
Используется только IE и Edge. Одна из причин, по которой он все еще используется, заключается в том, что он отлично работает в IE, позволяя событиям ввода, таким как ввод с клавиатуры и щелчки мышью, «пропускать очередь».setImmediate
выполняется до обратного вызова, в то время какsetTimeout
В IE не так много магии. (В конечном итоге Edge решил эту проблему, подробно описанную в предыдущем посте).
а также,setImmediate
Тот факт, что он существует в Node, означает, что большая часть кода «Node-polyfilled» использует его в браузере, но на самом деле не знает, что он делает. в узлеprocess.nextTick
а такжеsetImmediate
Разница сбивает с толку, дажеОфициальная документация для узлаГоворят, надо обменяться именами. (Однако для целей этой статьи я сосредоточусь на браузерах, а не на Node, поскольку я не эксперт по Node).
Принцип минимума: если вы знаете, что вам нужно делать, и пытаетесь оптимизировать производительность ввода IE, используйтеsetImmediate
. Если нет, не беспокойтесь. (или только в узле)
requestAnimationFrame
Теперь у нас есть один из самых важныхsetTimeout
Вместо этого таймер, который действительно висит в цикле рендеринга браузера. Кстати, если вы не знаете механизм цикла событий браузера, очень рекомендуюЭто выступление Джейка Арчибальда.
requestAnimationFrame
В основном это работает так: он работает сsetTimeout
Вроде как, но вместо того, чтобы ждать какое-то непредсказуемое количество времени (4 мс, 16 мс, 1 с и т. д.), он будет вызван при следующей перерисовке браузера. Теперь, как указал Джейк в своем выступлении, здесь есть небольшая проблема, в Safari, IE и Edge ниже 18 браузеров он выполняется после расчета стиля/макета. Но давайте проигнорируем это, потому что это не очень важная деталь.
я думаюrequestAnimationFrame
Он используется следующим образом: всякий раз, когда я знаю, что собираюсь изменить стиль или макет браузера — например, изменить свойство CSS или запустить анимацию — я вставлю его вrequestAnimationFrame
(сокращенно здесьrAF
). Это гарантирует несколько вещей:
- У меня меньше шансов испортить макет, потому что все изменения DOM ставятся в очередь и координируются.
- Мой код естественным образом адаптируется к характеристикам производительности браузера. Например, если здесь есть низкопрофильное устройство, пытающееся отобразить некоторые элементы DOM, rAF естественным образом замедлится по сравнению с обычным интервалом 16,7 мс (на экране с частотой 60 Гц), поэтому он не будет работать, как много setTimeout или setInterval, как сбой Устройство.
Вот почему библиотеки анимации, которые не полагаются на переходы CSS или ключевые кадры, такие какGreenSock or React Motion, обычно измененный в обратном вызове rAF. если элемент находится вopacity: 0
а такжеopacity: 1
анимировать переходы между ними, то нет смысла ставить в очередь миллиард обратных вызовов для обработки всех возможных промежуточных состояний, включаяopacity: 0.0000001
а такжеopacity: 0.9999999
.
Вместо этого вам лучше просто использоватьrAF
, позвольте браузеру сообщить вам, сколько кадров нужно отрисовать за заданный период времени, и выполнить вычисления для этого конкретного кадра. Таким образом, более медленные устройства естественным образом будут иметь медленную частоту кадров, а более быстрые устройства — высокую частоту кадров, если вы используете что-то вродеsetTimeout
С таким API, не зависящим от скорости рендеринга браузера, ничего из вышеперечисленного невозможно.
requestIdleCallback
rAF
Вероятно, самый полезный таймер в наборе инструментов, ноrequestIdleCallback
Также стоит упомянуть.Поддержка браузера не очень хорошая, но естьХороший рабочий полифилл(Нижний слой использует rAF).
Во многих случаяхrAF
похожий наrequestIdleCallback
. (сокращенно отсюда доrIC
)
картинаrAF
Такой же,rIC
будет естественным образом подстраиваться под характеристики браузера: если устройство перегружено,rIC
Может задержаться.rIC
Разница в том, что он загорается, когда браузер простаивает, то есть, когда браузер определяет, что у него нет других задач, микрозазных или входных событий для обработки, и вы можете делать все, что вы хотите. Это также дает вам «крайний срок» для отслеживания использованного бюджета, который является хорошей особенностью.
Дэн Абрамов вОтличный доклад на JSConf Iceland 2018, в разговоре он показывает, как использоватьrIC
. Во время разговора есть веб-приложение, которое будет вызываться при каждом вводе с клавиатуры пользователем.rIC
, который затем обновляет состояние рендеринга в обратном вызове. Это здорово, потому что быстро печатающий пользователь вызоветkeydown
/keyup
События запускаются очень быстро, но вы не хотите перерисовывать страницу при каждом нажатии клавиши.
Еще один хороший пример — индикатор «Осталось количество символов» в Twitter или MastoDon. существуетPinafore, Я используюrIC
сделать это, потому что мне все равно, будет ли индикатор перерисовываться для каждого ввода, который я делаю. Если я печатаю быстро, лучше всего расставить приоритеты при наборе текста, чтобы не потерять плавность.
В Pinafore небольшая всплывающая подсказка под полем ввода и всплывающая подсказка «Оставшиеся символы» обновляются по мере ввода.
Я заметил, чтоrIC
Это немного глючит в Chrome. В Firefox он запускается всякий раз, когда я интуитивно думаю, что браузер простаивает и готов выполнить какой-то код. (То же самое верно и для pollyfill.) Но в мобильном режиме Chrome для Android я заметил, что всякий раз, когда я касаюсь прокрутки, он прокручивается.rIC
С задержкой в несколько секунд браузер ничего не делает даже после того, как я только что коснулся экрана. (Я подозреваю, что проблема, которую я вижу,это.)
возобновить: Алекс Рассел из команды ChromeЗаметьте меняЭто известная ошибка, и она должна быть исправлена в ближайшее время!
в любом случае,rIC
еще один отличный инструмент. Я склонен думать так: использоватьrAF
Для критической работы по рендерингу используйтеrIC
для некритической работы по рендерингу.
дебаунс и дроссель
Здесь есть два не встроенных в браузер метода, но они полезны и их стоит знать. Если вы не знакомы с ними, вот одинОтличные советы и рекомендации по CSS
debounce
Стандартное использование находится вresize
Перезваниваю. Когда пользователь изменяет размер окна браузера, нет необходимостиresize
Макет обновляется в обратном вызове, потому что триггер слишком частый. Вместо этого вы можетеdebounce
Несколько сотен миллисекунд, что гарантирует, что обратный вызов сработает после того, как пользователь обработает размер окна.
throttle
, с другой стороны, это метод, который я использую больше. Например,scroll
События — отличный пример использования. Опять же, для каждогоscroll
Бессмысленно обновлять состояние просмотра по всему колбэку, потому что частота срабатывания слишком высока (частота различна для разных браузеров и разных методов ввода). использоватьthrottle
Это поведение можно нормализовать и гарантировать, что оно срабатывает только через каждые X миллисекунд. Вы можете настроить Lodashthrottle
(илиdebounce
) метод запуска задержки в конце времени или не запуска.
Вместо этого я бы не стал использовать в сценариях прокруткиdebounce
, потому что я не хочу, чтобы пользовательский интерфейс обновлялся только после того, как пользователь явно прекратил прокрутку. Потому что это может раздражать и сбивать с толку пользователя и пытаться прокручивать, чтобы постоянно обновлять состояние пользовательского интерфейса (например, в списке с бесконечной прокруткой).
Я использую его для различного пользовательского ввода и некоторых запланированных задач.throttle
, такие как очистка IndexedDB. Возможно, однажды он будет встроен в браузеры.
В заключение
Вот мой краткий обзор различных таймеров в браузере и способов их использования. Я мог что-то пропустить, так как здесь есть некоторые особенности (postMessage
илиlifecycle events
, что-нибудь еще? ). Но, надеюсь, это, по крайней мере, дает хороший обзор того, как я вижу таймеры в JavaScript.