Помните, как только Vue Mobile Tinence Оптимизация отсчета отсчет

Vue.js

предисловие

Обычно эффект обратного отсчета пишется с использованием setInterval, но это вызовет некоторые проблемы, самая распространенная проблема в том, что таймер работает неточно.

Если это просто обычный анимационный эффект, это не имеет значения, но если отсчет должен быть с точностью до миллисекунды, он не будет работать, иначе событие закончится, а обратный отсчет все равно будет идти в пользовательском интерфейсе. , но если вы не сможете принять участие в мероприятии, на вас пожалуются.

1. Предвидение знания

1. Установить интервальный таймер

Давайте поговорим о главном герое этой статьи, setInterval, веб-документ MDN объясняет это так:

Метод setInterval() многократно вызывает функцию или выполняет сегмент кода с фиксированной задержкой по времени между каждым вызовом.

Возвращает идентификатор интервала. (можно использовать для сброса таймера)

грамматика:let intervalID = window.setInterval(func, delay[, param1, param2, ...]);
пример:

Стоит отметить, что если это используется в setInterval,это указывает на объект окна, вы можете изменить указатель this с помощью таких методов, как вызов и применение.

setTimeout похож на setInterval, но выполняет функцию с задержкой в ​​n миллисекунд.однажды, и ручная очистка не требуется.

Что касается принципа работы setTimeout и setInterval, здесь задействовано другое понятие: цикл событий (event loop).

2. Цикл событий браузера

Во время выполнения JavaScript будет сгенерирована среда выполнения, эти среды выполнения будут последовательно добавлены в стек выполнения, при обнаружении асинхронного кода он будет приостановлен и добавлен в очереди задач (там разные задачи).

Как только стек выполнения станет пустым, цикл обработки событий извлечет код, который необходимо выполнить, из очереди задач и поместит его в стек выполнения для выполнения.

Благодаря циклу событий JavaScript может программировать асинхронно. (Но по сути это все еще синхронное поведение)

Давайте взглянем на классический вопрос интервью:

console.log('Script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

new Promise((resolve, reject) => {
  console.log('Promise');
  resolve()
}).then(() => {
  console.log('Promise 1');
}).then(() => {
  console.log('Promise 2');
});

console.log('Scritp end');

Порядок печати такой:

  1. "Script start"
  2. "Promise"
  3. "Script end"
  4. "Promise 1"
  5. "Promise 2"
  6. "setTimeout"

Что касается того, почему setTimeout установлен в 0, но печатается в конце, это включает в себя микрозадачи и макрозадачи в цикле событий.

2.1 Макрозадачи и микрозадачи

Различные источники задач будут назначены разным очередям задач, а источники задач можно разделить на микрозадачи и макрозадачи.

В ES6:

  • микрозадача называется Job
  • макрозадача называется Задача

macro-task(Task):Цикл событий имеет одну или несколько очередей задач. Источник задачи задачи очень широк, например загрузка ajax, событие щелчка, в основном все виды событий, которые мы часто связываем, являются источником задачи задачи и операциями с базой данных (IndexedDB), следует отметить, что setTimeout, setInterval, setImmediate также являются источником задач задачи. . Подводя итог источнику задачи задачи:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

micro-task(Job):Очередь микрозадач чем-то похожа на очередь задач. Обе очереди являются очередями «первым поступил — первым обслужен», и задачи предоставляются указанным источником задач. В цикле событий есть только одна очередь микрозадач. Кроме того, время выполнения микрозадач также отличается от времени выполнения макрозадач.

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

пс: микрозадачи не быстрее макрозадач

2.2 Последовательность выполнения цикла событий

  1. Выполнение синхронного кода (макрозадача);
  2. Стек выполнения пуст, а запрос должен быть выполнен;
  3. Выполнить все микрозадачи;
  4. визуализировать пользовательский интерфейс, если это необходимо;
  5. Затем запустите следующий раунд цикла событий, чтобы выполнить асинхронный код в задаче макроса;

ps: Если асинхронный код в макрозадаче имеет много вычислений и ему нужно манипулировать DOM, для более быстрого отклика интерфейса можно поместить операцию в микрозадачу.

Когда setTimeout выполняется в первый раз, он приостанавливается до задачи, ожидая следующего цикла цикла событий, и для выполнения цикла событий требуется не менее 4 мс, поэтому, даже еслиsetTimeout(()=>{...}, 0)Будет задержка 4 мс.

Поскольку JavaScript является однопоточным, ошибки setInterval/setTimeout не могут быть полностью устранены.

Это может быть событие в обратном вызове или оно может быть вызвано различными событиями в браузере.

Это также является причиной того, что таймер будет неточным, если страница работает в течение длительного времени.

Во-вторых, сцена проекта

Я столкнулся с необходимостью обратного отсчета в проекте компании, но у некоторых предшественников были написаны компоненты.Поскольку срок проекта небольшой, они использовали его напрямую.Однако в процессе использования были обнаружены некоторые баги:

  1. На тестовой машине Android, когда палец скользит или вот-вот соскользнет, ​​миллисекунды остановятся, и он продолжит ходить после того, как отпустит его;
  2. После перехода на другую страницу и последующего возврата обратный отсчет идет неправильно в минутах и ​​секундах;
  3. После возврата на исходную страницу повторные запросы данных приведут к ускорению обратного отсчета;

Первая ошибка связана с тем, что скольжение блокирует основной поток, из-за чего макрозадача не выполняется нормально.

Вторая ошибка заключается в том, что после переключения страниц, чтобы снизить потребление производительности, браузер автоматически продлевает интервал таймера предыдущей страницы, что приводит к нарастающей ошибке.

Третья ошибка заключается в том, что таймер не был очищен до вызова метода, поэтому при отслеживании метки времени был добавлен новый таймер.

В этой статье рассматриваются первые две ошибки.

Проверил много статей, есть два общих решения:

1. requestAnimationFrame()

Веб-документ MDN объясняет это следующим образом:

window.requestAnimationFrame() сообщает браузеру, что вы хотите выполнить анимацию, и просит браузер вызвать указанную функцию обратного вызова, чтобы обновить анимацию перед следующей перерисовкой. Этот метод должен передать функцию обратного вызова в качестве параметра, функция обратного вызова будет выполнена до следующей перерисовки браузера.

Примечание. Если вы хотите продолжить обновление следующего кадра анимации перед следующей перерисовкой браузера, сама функция обратного вызова должна снова вызвать window.requestAnimationFrame().

Частота выполнения requestAnimationFrame() зависит от частоты обновления экрана браузера.Обычный экран 60Гц или 75Гц, то есть он может перерисовываться максимум 60 или 75 раз в секунду.Основная идея requestAnimationFrame такова чтобы не отставать от этой частоты обновления. , используйте эту частоту обновления для перерисовки страницы. Кроме того, при использовании этого API обновления автоматически останавливаются, когда страница не находится на текущей вкладке браузера. Это экономит CPU, GPU и энергию.

Но будьте осторожны: requestAnimationFrame выполняется в основном потоке. Это означает, что если основной поток очень занят, эффект анимации requestAnimationFrame будет значительно уменьшен.

Использование requestAnimationFrame может в определенной степени заменить setInterval, но временной интервал необходимо рассчитать.Если рассчитать по частоте обновления экрана (fps) 60 Гц, 1000/60 = 16,6666667 (мс), то есть выполняется каждые 16,7 мс, но fps не фиксировано, игроки, которые играли в FPS (шутеры от первого лица), будут иметь глубокое понимание. Однако по сравнению с предыдущим setInterval без какой-либо оптимизации ошибка намного меньше исходной.

Мое решение состоит в том, чтобы установить переменную затем, после выполнения функции анимации, записать текущую временную метку, а при входе в функцию анимации в следующий раз вычесть [затем] из [текущей временной метки], чтобы получить временной интервал, а затем пусть [Обратный отсчет Отметка времени] минус [интервал] и записывайте время ухода при выходе со страницы, что еще больше уменьшает ошибку.

<script>
export default {
  name: "countdown",
  props: {
    timestamp: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      remainTimestamp: 0
      then: 0
    };
  },
  activated () {
    window.requestAnimationFrame(this.animation);
  },
  deactivated() {
    this.then = Date.now();
  },
  methods: {
    animation(tms) {
      if (this.remainTimestamp > 0 && this.then) {
        this.remainTimestamp -= (tms - this.then); // 减去当前与上一次执行的间隔
        this.then = tms; // 记录本次执行的时间
        window.requestAnimationFrame(this.animation);
      }
    }
  },
  watch: {
    timestamp(val) {
      this.remainTimestamp = val;
      this.then = Date.now();
      window.requestAnimationFrame(this.animation);
    }
  }
};
</script>

Во время использования requestAnimationFrame по-прежнему отличается от setInterval.Самое большое отличие заключается в том, что время интервала нельзя настроить.

Если обратный отсчет должен быть точным только до секунды, то выполнение 16,7 раз за 1000 мс будет слишком расточительным для производительности. И если вы хотите имитировать setInterval, вам нужны дополнительные переменные для обработки интервала, что также снижает читабельность кода.

Итак, попробуйте второй вариант: Web Worker.

2. Web Worker

Web Worker — это черная технология, реализующая многопоточность в JavaScript, объяснение в блоге Ruan Yifeng выглядит следующим образом:

Язык JavaScript использует однопоточную модель, что означает, что все задачи могут выполняться только в одном потоке, и одновременно может выполняться только одна задача. Предыдущие задачи не выполнены, а последние задачи можно только ждать. С увеличением вычислительной мощности компьютера, особенно с появлением многоядерных ЦП, однопоточность приносит большие неудобства и не может в полной мере использовать вычислительную мощность компьютера.
Роль Web Worker заключается в создании многопоточной среды для JavaScript, позволяющей основному потоку создавать рабочие потоки и назначать последним некоторые задачи для выполнения. Пока работает основной поток, рабочий поток работает в фоновом режиме, не мешая друг другу. Подождите, пока рабочий поток завершит задачу вычисления, а затем верните результат в основной поток. Преимущество этого в том, что некоторые ресурсоемкие задачи или задачи с высокой задержкой ложатся на рабочий поток, а основной поток (обычно отвечающий за взаимодействие с пользовательским интерфейсом) будет работать плавно и не будет блокироваться или замедляться.
Как только рабочий поток создан, он всегда будет работать и не будет прерываться действиями в основном потоке (такими как нажатие пользователем кнопки, отправка формы). Это способствует ответу на сообщение основного потока в любое время. Однако это также делает Worker более ресурсоемким, его нельзя использовать слишком часто, и его следует закрывать после того, как он израсходован.

Конкретные уроки можно увидетьБлог Руан Ифэна такжеMDN — использование веб-воркеров,Больше никогда.

Но чтобы использовать Web Worker в проекте Vue, все равно придется немного повозиться.

Первый — это загрузка файла, официальный пример такой:

var myWorker = new Worker('worker.js');

Поскольку рабочие не могут читать локальные файлы, этот скрипт должен исходить из сети. Если загрузка не удалась (например, ошибка 404), рабочий процесс завершится неудачно.

Поэтому мы не можем импортировать напрямую импортом, иначе файл не будет найден, поэтому Google нашел, что есть два решения;

2.1 vue-worker

Это плагин, написанный автором simple-web-worker для проектов Vue, который может вызывать такие функции, как Promise.

Адрес на гитхабе:vue-worker

Но при использовании были обнаружены некоторые проблемы, то есть setInterval не будет выполняться:

Входящий val — это оставшаяся метка времени обратного отсчета, но операция обнаружила, что возвращаемый val не изменился, то есть setInterval не выполнился. Теоретически Web Worker сохранит setInterval. (Может у меня что-то не так с осанкой? Ходил по вопросам, а ответа до сих пор нет. Есть здоровяк, что посоветуете?)

Основной setInterval обратного отсчета не может быть выполнен, поэтому этот плагин устарел и выполняется план Б.

2.2 worker-loader

Это плагин для экранирования файлов JavaScript, похожий на Babel-Loader, и его конкретное использование было обобщено и не будет повторяться:

Как использовать Web Worker под ES6+Webpack

Вставьте код напрямую:

timer.worker.js:

self.onmessage = function(e) {
  let time = e.data.value;
  const timer = setInterval(() => {
    time -= 71;
    if(time > 0) {
      self.postMessage({
        value: time
      });
    } else {
      clearInterval(timer);
      self.postMessage({
        value: 0
      });
      self.close();
    }
  }, 71)
};

countdown.vue:

<script>
import Worker from './timer.worker.js'
export default {
  name: "countdown",
  props: {
    timestamp: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      remainTimestamp: 0
    };
  },
  beforeDestroy () {
    this.worker = null;
  },
  methods: {
    setTimer(val) {
      this.worker = new Worker();
      this.worker.postMessage({
        value: val
      });
      const that = this;
      this.worker.onmessage = function(e) {
        that.remainTimestamp = e.data.value;
      }
    }
  },
  watch: {
    timestamp(val) {
      this.worker = null;
      this.setTimer(val);
    }
  }
};
</script>

Тут есть небольшой эпизод.При локальном запуске проблем нет, но при упаковке сообщается об ошибке.Причина расследования - правила worker-loader написаны за babel-loader.Результат совпадает с . js файл сначала и напрямую помещает .worker .js был обработан с помощью babel-loader, что привело к сбою импорта рабочего процесса, и было сообщено об ошибке упаковки:

webpack.base.conf.js (проект компании относительно старый, и метод настройки webpack 4.0+ не используется, но принцип тот же)

  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          vueLoaderConfig,
          postcss: [
            require('autoprefixer')({
              browsers: ['last 10 Chrome versions', 'last 5 Firefox versions', 'Safari >= 6', 'ie > 8']
            })
          ]
        }
      },
      {
        // 匹配的需要写在前面,否则会打包报错
        test: /\.worker\.js$/,
        loader: 'worker-loader',
        include: resolve('src'),
        options: {
          inline: true,    // 将 worker 内联为一个 BLOB
          fallback: false, // 禁用 chunk
          name: '[name]:[hash:8].js'
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [utils.resolve('src'), utils.resolve('test')]
      },
      // ...
    ]
  },

3. Резюме

После некоторых метаний у меня появилось более глубокое понимание цикла событий браузера.Не только задачи таймера, такие как setInterval, но и другие высокоинтенсивные вычисления также могут быть обработаны многопоточностью, но обратите внимание на закрытие потока после обработки, иначе это будет серьезно потреблять ресурсы. Однако обычная анимация должна выполняться с помощью requestAnimationFrame или CSS-анимации, насколько это возможно, чтобы максимально улучшить беглость страницы.

Я впервые пишу технический блог, и у меня мало знаний. Неизбежны упущения. Если есть лучшее решение обратного отсчета, вы можете дать совет.

Использованная литература:

  1. Механизм цикла событий браузера
  2. Учебное пособие по веб-воркеру - Жуан Ифэн
  3. официальная документация worker-loader
  4. Как использовать Web Worker под ES6+Webpack