Часто при просмотре ресурсов для JavaScript мы видим термин «состояние гонки», используемый для описания определенного неожиданного поведения. Но действительно ли такое поведение неожиданно, и действительно ли это состояние гонки? Давайте копнем глубже и узнаем, что происходит на самом деле.

Что такое асинхронный Javascript?

Скорее всего, вы уже сталкивались с асинхронным JavaScript в виде обработчиков кликов для кнопок или запросов AJAX, но вы можете не знать, как он обрабатывается за кулисами. Принимая во внимание, что синхронный код выполняется в том порядке, в котором он написан в исходном коде:

functionA(); // always executed first
functionB(); // always executed second

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

// The first callback function.
function onClickA() {
  console.log('button 1 clicked!');
}
// The second callback function.
function onClickB() {
  console.log('button 2 clicked!');
}
// Setting up the callbacks, this is done synchronously.
button1.addEventListener('click', onClickA); // always added first
button2.addEventListener('click', onClickB); // always added second

Часть настройки выполняется синхронно, поэтому прослушиватель событий кнопки 1 всегда добавляется первым, а затем — кнопки 2. Однако функция обратного вызова запускается только тогда, когда пользователь нажимает кнопку, а не в тот момент, когда она появляется в исходном коде.

Это различие очень важно понять. Асинхронный код — это when-then система: вы устанавливаете начальное when условие, и когда оно возникает, вы затем выполняете then часть. Например, «при нажатии кнопки запустить эту функцию». «Когда Боб пригласит меня на обед, я буду есть с ним». «Когда Салли положит файл на мой стол, я начну над ним работать».

Это может звучать очень похоже на использование операторов if-then в синхронном коде, но разница в том, что в асинхронном коде у вас нет контроля над тем, когда возникает условие when, вы можете только ждать, пока оно выполнится. случиться и реагировать на это. Это разница между тем, чтобы постоянно спрашивать Боба, готов ли он к обеду, и ожиданием, когда он скажет вам, или постоянно спрашивать файл у Салли, и ждать, пока она положит его на ваш стол, или постоянно проверять, нажал ли пользователь кнопку. вместо того, чтобы ждать, пока браузер сообщит вам.

Именно по этой причине асинхронный JavaScript так хорошо подходит для браузеров. Вместо того, чтобы постоянно проверять, взаимодействует ли пользователь с браузером, мы просто устанавливаем условие when и ждем, пока браузер сообщит нам, когда это произошло. А пока мы можем выполнять любой другой код.

Ну а как же АЯКС?

Безусловно, наиболее распространенное использование «состояния гонки» — это описание того, что происходит, когда выполняется несколько одновременных запросов AJAX. Возьмите это, например:

// Request profile for user 1.
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(function(response) {
  div.innerText = response.text();
});
// Request profile for user 2.
fetch('https://jsonplaceholder.typicode.com/users/2')
.then(function(response) {
  div.innerText = response.text();
});

На первый взгляд может показаться, что div будет содержать профиль пользователя 2; в конце концов, вызовы выглядят синхронно, а запрос профиля пользователя 2 идет после запроса пользователя 1. Однако это асинхронный вызов. Когда мы запускаем функцию fetch(), мы только устанавливаем условие when, а код then() выполняется только после того, как браузер сообщил нам, что сервер ответил данными. Это будет более очевидно, если вместо этого мы используем XMLHTTPRequest:

for (let i = 1; i <= 2; i++) {
  // Create an XHR object.
  let xhr = new XMLHTTPRequest();
  // Configure the XHR with the URL we want to hit.
  xhr.open('GET', 'https://myserver.com/users/' + i);
  // WHEN we receive the response, THEN run the anonymous callback
  // function.
  xhr.addEventListener('readystatechange', function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
      div.innerText = xhr.responseText;
    }
  });
  // Instruct the browser to start the request. This is a
  // SYNCHRONOUS call that returns immediately because the browser
  // is the one doing the actual request.
  request.send();
}

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

Этот последний пункт заставляет людей думать, что запросы AJAX могут привести к условиям гонки: в конце концов, если вы не можете быть уверены, в каком порядке будут вызываться функции обратного вызова, как вы можете узнать, какой профиль пользователя будет отображаться в див?

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

let count = 0
for (let i = 1; i <= 2; i++) {
  // Create the button.
  let button = document.createElement('button');
  // Configure the button ith the text we want it to display.
  button.textContent = 'Set count to ' + i;
  // WHEN the button is clicked, THEN run the anonymous callback
  // function.
  button.addEventListener('click', function() {
    count = i;
  }
  // Instruct the browser to render the button. This is a
  // SYNCHRONOUS call that returns immediately because the browser
  // is the one doing the rendering.
  document.body.appendChild(button);
}

Каким будет значение count после того, как пользователь нажмет обе кнопки, 1 или 2? Это зависит от порядка, в котором пользователь нажимал кнопки, что находится вне вашего контроля. В этом случае незнание окончательного значения count для двух кнопок точно такое же, как незнание конечного значения в div из двух запросов AJAX.

Из приведенного выше кода видно, что создание кнопки, добавление прослушивателя событий click, а затем передача ее браузеру для рендеринга на самом деле ничем не отличается от создания кнопки XMLHTTPRequest, добавления прослушивателя событий readystatechange, а затем передачи ее в браузер для отправки. Оба они представляют собой асинхронный код, который устанавливает условие when и выполняет блок then, когда что-то внешнее сообщает ему, что условие выполнено.

А как насчет состояния готовности DOM и setTimeout()?

Они не так часто встречаются при использовании с состоянием гонки, поэтому я просто кратко пройдусь по ним. Когда браузер встречает тег <script>, он загружает и выполняет его везде, где он отображается в HTML. Этот график от Google показывает, что браузер делает внутри:

С простым тегом <script> браузер приостанавливает синтаксический анализ HTML, чтобы загрузить и выполнить сценарий, прежде чем продолжить. Если DOM не готов к этому моменту (а в большинстве случаев так и не будет), скрипт выдаст ошибку. Называть это состоянием гонки все равно, что говорить, что попытка запустить исполняемый файл до завершения загрузки является состоянием гонки.

В большинстве случаев люди хотят добавить атрибут defer, где скрипт загружается параллельно и выполняется только после полного завершения анализа HTML. Добавление тега <script> в конец тела выполняет то же самое, за исключением того, что загрузка также выполняется после завершения синтаксического анализа HTML.

Что касается использования setTimeout() и получения неожиданного поведения (что-то, что эта книга использует в качестве примера условия гонки), это связано с тем, что setTimeout() выполняет функцию только по истечении настроенного количества времени:

let xhr = new XMLHTTPRequest();
xhr.open('GET', 'https://myserver.com/users/1');
xhr.send();
// Add the event listener after 3 seconds.
setTimeout(function() {
  xhr.addEventListener('readystatechange', ...);
}, 3000);

Сервер мог бы выполнить запрос менее чем за 3 секунды, после чего прослушиватель событий readystatechange еще не был добавлен. Называть это состоянием гонки — все равно, что говорить, что автобус, который доезжает до остановки без пассажиров, продолжает движение и не подбирает пассажира, пришедшего позже, является состоянием гонки.

Роза под любым другим именем

Если все это не новость для вас, вы можете подумать: Это именно то, что представляет собой состояние гонки JavaScript, и вы будете правы в зависимости от того, как вы определяете состояние гонки. Википедия дает следующее определение:

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

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

Скорее, лучше придерживаться того, как в разговорной речи используется термин «состояние гонки», который относится к нежелательному побочному эффекту, заключающемуся в том, что что-то еще использует ресурс в то же время, что и вы, что приводит к непредсказуемости вашего результата. Например, если вы используете калькулятор, и пока вы его используете, призрак случайным образом нажимает кнопки на нем. Вы не можете видеть призрака или нажатия его кнопок, но это влияет на ваши окончательные расчеты.

В случае асинхронного JavaScript, поскольку он однопоточный, каждая функция обратного вызова выполняется до завершения, прежде чем запустится следующая, и вывод будет «непредсказуемым», только если вы смотрите на него через разные функции. Точно так же, как если группа студентов использует один калькулятор один за другим, вы не можете предсказать, какое конечное значение будет на калькуляторе, потому что оно зависит от порядка, в котором студенты использовали его.

По этой причине я считаю, что использование «условий гонки» для описания асинхронного JavaScript является неправильным. Однако на самом деле нет лучшего, общепринятого термина, поскольку он является частью основного определения того, что такое асинхронное программирование (код не выполняется немедленно, только когда его запускает внешнее неконтролируемое событие). Я бы хотел, чтобы мы, как сообщество, отказались от термина «состояние гонки», когда мы описываем понятие, совершенно отличное от того, как этот термин используется в других языках программирования.

Я надеюсь, что эта статья поможет прояснить некоторые моменты! С нетерпением ждите части 2, где я объясню, как решить распространенные проблемы при работе с асинхронным кодом.