В предыдущей статье этой серии мы показали, как реализовать ожидание с обещаниями — мы хотели эффективно наблюдать за каким-то условием и реагировать, когда оно становится истинным. В данном случае мы делаем что-то похожее, но другое: мы хотим выполнить опрос API, по сути асинхронную задачу, и в конечном итоге действовать при необходимости.

Если подробнее, ситуация такая. Мы хотим опросить некоторый API и проверить результаты. Если результаты удовлетворяют какому-то условию, мы хотим проделать специальную работу. Например, мы могли бы дождаться доступности некоторых данных; когда это произойдет, мы захотим что-то сделать (обновить экран? выполнить дополнительную работу?) и прекратить опрос.

В нашей статье об ожидании мы основывали все решения на обещаниях. Здесь, хотя у нас также будет решение, основанное на обещаниях, мы также будем использовать другие методы и функции для разнообразия и расширения нашего набора инструментов. А именно, мы увидим исследование:

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

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

Готовиться

Для разработки наших примеров нам понадобятся несколько фальшивых обещаний (мы не хотим использовать настоящие), поэтому мы будем повторно использовать код из предыдущих статей.

const success = (time, value) =>
  new Promise((resolve) => setTimeout(resolve, time, value));

const failure = (time, reason) =>
  new Promise((_, reject) => setTimeout(reject, time, reason));

Первая функция возвращает обещание, которое будет разрешено в value после заданного time, а вторая функция возвращает обещание, которое будет отклонено (с заданным reason) после некоторого time. Все время должно быть указано в миллисекундах согласно стандарту JavaScript.

Мы также хотим, чтобы время прошло, поэтому мы также будем использовать следующее:

const timeout = (time) =>
  new Promise((resolve) => setTimeout(resolve, time));

Это создает обещание, которое разрешается само через некоторое время time. Чтобы задержаться на три секунды, мы должны написать:

await timeout(3000);

В оставшейся части статьи мы будем имитировать вызов API примерно так: он ждет 0,4 секунды, а затем выдает время и результат "all ok".

const callFakeApi = () => {
  console.log(new Date(), "Calling API");
  return success(400, "all ok");
};

Чтобы проверить результаты, мы будем использовать такие функции, как следующие: они имитируют, что условие несколько раз не выполнено, а затем выполняется успешно. Мы хотим иметь некоторое разнообразие; если бы опрос удался с первой попытки, мы не смогли бы по-настоящему проверить нашу логику.

let count = 0;

const testCondition = () => {
  count++;
  console.log(new Date(), "Testing", count, count === 4 ? "OK" : "Not yet...");
  return count === 4;
};

Работа, которую мы проделаем, абсолютно пуста — всякий раз, когда наш смоделированный вызов API завершается успешно, мы записываем время и некоторый текст.

const doSomething = () => console.log(new Date(), "Doing something...");

Теперь мы готовы; давайте начнем рассматривать решения нашей проблемы с опросом. Мы хотим иметь какой-то способ начать опрос, а также способ остановить его, если какое-то условие в нашем приложении определит, что нам не следует продолжать это. Мы увидим три различных способа этого кодирования; дело не в том, что нам нужно так много версий, а в том, что мы будем использовать разные методы и функции JavaScript, так что это будет полезный опыт.

Первое решение с циклом

Давайте воспользуемся самым простым решением: мы создаем цикл, в котором ждем некоторое время, вызываем API, проверяем условие и продолжаем цикл до тех пор, пока условие не будет выполнено или опрос не будет отменен.

Наша функция получит четыре параметра — и они будут одинаковыми для других реализаций опроса, которые мы увидим:

  • callApiFn — это функция, которая будет вызывать внешний API
  • testFn будет проверять все, что возвращает API; если этот тест выполнен, это означает, что мы должны что-то сделать
  • doFn — это функция, которая будет вызываться при удовлетворительном результате testFn.
  • time — это задержка между вызовами опроса.

Давайте теперь посмотрим фактический код нашей функции startPolling:

function startPolling(callApiFn, testFn, doFn, time) {
  let polling = true;                                         // [1]

  (async function doPolling() {
    while (polling) {                                         // [2]
      try {
        let result;
        if (polling) {                                        // [3] 
          await timeout(time);
        }
        if (polling) {                                        // [4]
          let result = await callApiFn();
        }
        if (polling && testFn(result)) {                      // [5]
          stopPolling();
          doFn(result);
        }
      } catch (e) {                                           // [6]
        stopPolling();
        throw new Error("Polling cancelled due to API error");
      }
    }
  })();                                                       // [7]

  function stopPolling() {                                    // [8]
    if (polling) {
      console.log(new Date(), "Stopping polling...");
      polling = false;
    } else {
      console.log(new Date(), "Polling was already stopped...");
    }
  }

  return stopPolling;                                         // [9]
}

Давайте пройдемся по коду.

  1. мы определяем переменную polling; если оно становится ложным, мы прекращаем опрос.
  2. мы устанавливаем цикл, который будет продолжаться до тех пор, пока опрос не будет успешным или прерван.
  3. почему мы тестируем polling? Помните, что мы используем асинхронный код; несмотря на while в (2), опрос сейчас можно отменить. Если опрос продолжается, мы берем time перед фактическим вызовом API.
  4. Мы вызываем API, если все еще работаем (см. предыдущий пункт) по истечении time.
  5. опять же, если опрос не был отменен, проверяем результаты API; если они были удовлетворительными, прекратите опрос и сделайте все, что ожидалось.
  6. если произошла какая-то ошибка, мы прекращаем опрос и выдаем исключение
  7. функция doPolling на самом деле является IIFE; мы определяем функцию, давая ей имя просто для ясности, и сразу вызываем ее
  8. функция stopPolling позволит нам отменить опрос, если он запущен; установка polling на false останавливает работу.
  9. Результатом вызова startPolling() является функция, которая нам нужна для отмены опроса, если мы захотим.

Как бы мы это использовали? Ниже приводится пример:

console.log(new Date(), "Starting polling");
const stopPolling = startPolling(callFakeApi, testCondition, doSomething, 1000);
await timeout(6300);
console.log(new Date(), "Canceling polling");
stopPolling();

Запуск этого производит:

2023-03-20T03:04:50.530Z Starting polling
2023-03-20T03:04:51.540Z Calling API
2023-03-20T03:04:51.741Z Testing 1 Not yet...
2023-03-20T03:04:52.744Z Calling API
2023-03-20T03:04:52.945Z Testing 2 Not yet...
2023-03-20T03:04:53.946Z Calling API
2023-03-20T03:04:54.147Z Testing 3 Not yet...
2023-03-20T03:04:55.149Z Calling API
2023-03-20T03:04:55.350Z Testing 4 OK
2023-03-20T03:04:55.351Z Stopping polling...
2023-03-20T03:04:55.351Z Doing something...
2023-03-20T03:04:56.839Z Canceling polling
2023-03-20T03:04:56.840Z Polling was already stopped...

Давайте подробно рассмотрим этот вывод; другие реализации в статье дадут аналогичные результаты.

  • мы начинаем опрос каждую 1 секунду (1000 миллисекунд)
  • через секунду после старта API вызывается первый раз
  • после (фальшивой) задержки результаты приходят, но тест не пройден, поэтому мы продолжаем опрос
  • еще два раза вызываем API, но тест не проходит
  • четвертый раз тест проходит успешно; мы «что-то делаем», и голосование прекращается
  • при попытке отменить опрос (через 6,3 секунды после его запуска) мы обнаруживаем, что опрос уже остановлен

Итак, как мы видим, код работает хорошо. (Это решение похоже на второе решение «сон и цикл», которое мы видели в статье «Ожидание с обещаниями».) Если мы запустим этот цикл опроса, он продолжит работать асинхронно (так что вы можете сделать что-то еще), но должен мы получаем ответ API, который проходит некоторую проверку, будет предпринято определенное действие. Конечно, ничто не запрещает начать голосование снова; это зависит от вас. Ключевым моментом является то, что наша логика работает хорошо — давайте теперь рассмотрим альтернативные версии.

Повтор сеанса для разработчиков

Выявляйте разочарования, выявляйте ошибки и устраняйте замедления, как никогда раньше, с помощью OpenReplay — инструмента воспроизведения сеансов с открытым исходным кодом для разработчиков. Разместите его самостоятельно за считанные минуты и получите полный контроль над данными своих клиентов. Загляните в наш репозиторий GitHub и присоединяйтесь к тысячам разработчиков в нашем сообществе.

Второе решение с интервалами

Решение, которое мы только что видели, работает хорошо, но есть более очевидный способ работы — чтобы неоднократно делать что-то через определенные промежутки времени, мы должны использовать собственную функцию setInterval() JavaScript.

Мы напишем новую функцию startPolling() с теми же параметрами и характеристиками, что и в предыдущем разделе.

import {
  timeout,
  callFakeApi,
  testCondition,
  doSomething,
} from "./pollingCommon.mjs";

function startPolling(callApiFn, testFn, doFn, time) {
  let intervalId = setInterval(() => {                        // [1]
    callApiFn()                                               // [2]
      .then((data) => {
        if (intervalId && testFn(data)) {                     // [3]
          stopPolling();
          doFn(data);
        }
      })
      .catch((e) => {                                         // [4]
        stopPolling();
        throw new Error("Polling cancelled due to API error");
      });
  }, time);

  function stopPolling() {                                    // [5]
    if (intervalId) {
      console.log(new Date(), "Stopping polling...");
      clearInterval(intervalId);
      intervalId = null;
    } else {
      console.log(new Date(), "Polling was already stopped...");
    }
  }

  return stopPolling;                                         // [6]
}

Как это работает? Давайте посмотрим детали.

  1. мы используем setInterval() для создания вечного цикла, который периодически вызывает API, проверяет его результаты и т. д. При отмене опроса intervalId будет null.
  2. мы вызываем API
  3. когда приходят результаты, если опрос не был отменен, мы проверяем результаты, и если тест пройден, мы прекращаем опрос и делаем что-то особенное
  4. при любой ошибке API мы прекращаем опрос и выдаем исключение
  5. чтобы остановить опрос (если он был запущен) мы используем clearInterval() и устанавливаем intervalId на null, как мы упоминали выше относительно пункта (1)
  6. как и в случае с циклическим опросом, мы возвращаем функцию, которую можно использовать для остановки опроса.

Если мы запустим этот код, мы получим точно такой же результат, как и в предыдущем разделе, а именно:

2023-03-20T03:17:09.981Z Starting polling
2023-03-20T03:17:10.990Z Calling API
2023-03-20T03:17:11.192Z Testing 1 Not yet...
2023-03-20T03:17:11.992Z Calling API
2023-03-20T03:17:12.193Z Testing 2 Not yet...
2023-03-20T03:17:12.993Z Calling API
2023-03-20T03:17:13.194Z Testing 3 Not yet...
2023-03-20T03:17:13.993Z Calling API
2023-03-20T03:17:14.194Z Testing 4 OK
2023-03-20T03:17:14.195Z Stopping polling...
2023-03-20T03:17:14.196Z Doing something...
2023-03-20T03:17:16.290Z Canceling polling
2023-03-20T03:17:16.290Z Polling was already stopped...

Итак, реализация другая (в данном случае код похож на третье решение, которое мы видели в статье «Ожидание с промисами»), но производительность и результаты те же. Давайте рассмотрим третье решение, несколько иное, основанное на обещаниях.

Третье решение с обещаниями

Давайте теперь разработаем третий подход. Мы могли бы создать обещание, которое будет выполнено, когда опрос заработает и условие будет выполнено. (Если вызовы API завершатся неудачей или опрос будет отменен, обещание будет отклонено.) Мы можем использовать метод JavaScript setTimeout(), чтобы обеспечить некоторую задержку между попытками опроса, следующим образом:

function startPolling(callApiFn, testFn, time) {
  let polling = false;                                        // [1]
  let rejectThis = null;

  const stopPolling = () => {                                 // [2]
    if (polling) {
      console.log(new Date(), "Polling was already stopped...");
    } else {
      console.log(new Date(), "Stopping polling...");         // [3]
      polling = false;
      rejectThis(new Error("Polling cancelled"));
    }
  };

  const promise = new Promise((resolve, reject) => {          
    polling = true;                                           // [4]
    rejectThis = reject;                                      // [5]

    const executePoll = async () => {                         // [6]
      try {
        const result = await callApiFn();                     // [7]
        if (polling && testFn(result)) {                      // [8]
          polling = false;
          resolve(result);
        } else {                                              // [9]
          setTimeout(executePoll, time);
        }
      } catch (error) {                                       // [10]
        polling = false;
        reject(new Error("Polling cancelled due to API error"));
      }
    };

    setTimeout(executePoll, time);                            // [11]
  });

  return { promise, stopPolling };                            // [12]
}

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

console.log(new Date(), "Starting polling");
const { promise, stopPolling } = startPolling(callFakeApi, testCondition, 1000);
promise.then(doSomething).catch(() => { /* do something on error */ });
await timeout(6300);
console.log(new Date(), "Canceling polling");
stopPolling();

Когда мы вызываем startPolling(), мы получаем promise (для выполнения опроса) и stopPolling() функцию (для отмены опроса в любой момент). Промис начнет выполняться, и если вызов API завершится успешно и testCondition() вернет true, запустится метод then(), и здесь мы сделаем все необходимое. (При любой ошибке, как обычно, будет вызван метод catch().) Результат этого кода снова такой же, как и в двух предыдущих примерах, так что давайте не будем тратить зря пространство; вместо этого мы изучим код.

  1. Мы не будем передавать функцию doFn(), поскольку она будет вызываться в методе обещания then(). У нас также будет переменная polling (она будет истинной, пока продолжается опрос) и переменная rejectThis для хранения функции reject для обещания; см. (3) и (4) ниже
  2. функция stopPolling() проверит, выполняется ли опрос, и если да, то остановит его
  3. чтобы остановить опрос, мы сбрасываем polling на false и используем сохраненную функцию rejectThis, чтобы заставить обещание немедленно отклониться
  4. мы создаем новое обещание; он начинается с установки polling в значение true, чтобы показать, что опрос выполняется.
  5. чтобы иметь возможность принудительно отклонить обещание в stopPolling, нам нужно сохранить параметр reject в rejectThis; см. (3) выше
  6. executePoll выполнит опрос и тестирование
  7. мы вызываем API
  8. если опрос не был отменен, мы тестируем результаты API, и если тест пройден, мы прекращаем опрос и разрешаем промис
  9. если тест не прошёл, устанавливаем новый опрос после задержки time
  10. при любой ошибке мы прекращаем опрос и отклоняем обещание
  11. мы начинаем опрос, настроив executePoll на первый запуск после задержки time
  12. эта функция возвращает два значения: обещание, которое выполняет опрос, и функцию, останавливающую опрос.

Эта третья реализация немного отличается (и чем-то похожа на пятое решение в статье «Ожидание с обещаниями», но здесь мы использовали setTimeout() вместо setInterval()), но нам удалось показать еще третий способ реализации желаемого опроса.

Подведение итогов

Мы увидели три различных способа реализации решения для опроса: циклы, интервалы и обещания. Даже если нам не нужны все эти разные решения (подойдут любые!), мы получили возможность изучить три альтернативные реализации, а также сравнить результаты с предыдущими из статьи Ожидание с обещаниями; такого рода сравнительное исследование обычно весьма полезно, поэтому просмотрите и эту статью!

Оригинально опубликовано на https://blog.openreplay.com.