Тур по таймеру JavaScript "Перевод"

Node.js внешний интерфейс JavaScript браузер

Тур по 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). Это гарантирует несколько вещей:

  1. У меня меньше шансов испортить макет, потому что все изменения DOM ставятся в очередь и координируются.
  2. Мой код естественным образом адаптируется к характеристикам производительности браузера. Например, если здесь есть низкопрофильное устройство, пытающееся отобразить некоторые элементы 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.