Browser Event Loop

Модель конкуррентности в JS называется Event Loop и отличается от моделей конкурретности в таких языках как С++ и Java.

JS имеет однопточный рантайм, выполняющий одну вещь за раз. Каждая вкладка в браузере выполняется в отдельном процессе своим циклом событий (event loop - EL).

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

  • Разбор HTML

  • Выполнение JS кода в script тегах

  • Обработка реакций на действия пользователя (движения мыши, щелчки кнопок)

  • Обработка результатов (асинхронных) сетевых запросов.

Схематически, JS имеет следующий рантайм:

  • Выполнение функций формирует стек вызова.

  • Куча используется для хранения объектов (большой, неструктурированный регион памяти)

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

Очередь событий извлекает по одной задаче и выполняет её. EL использует последовательную (run-to-completion) семантику: текущая выполняемая задача всегда завершается полностью до того как следующая задача начнет выполняться. Таким образом, каждая задача имеет полный контроль над текущим состоянием программы и программисту нет волноваться по поводу конкуррентных модификаций. По завершению текущей задачи, из очереди событий извлекается следующая и передается на исполнение в JS-Runtime.

Параллельно с EL выполняется множество других фоновых процессов (таймеры, обработка ввода), которые взаимодействуют с EL помещая в его очередь задач новые задачи на выполнение. Эти фоновые процессы запускаются как самим браузером, так и из текущего пользовательского кода. Например, выполнение запроса из JS-кода создает фоновую задачу, выполняюуюся в WebApi браузера. По завершению выполнения запроса WebApi помещает задачу обработки запроса в EL.

Все задачи, связанные с JS (script и сетевые запросы) выполняются внутри встроенного в браузер JS-движка. Js-задачи завершаются вместе с завершением выполнения соответствующего им кода. JS-движок обеспечивает выполнение JS-кода, хранит стек вызовов, управляет кучей и т.п.

A web worker or a cross-origin iframe has its own stack, heap, and message queue. Two distinct runtimes can only communicate through sending messages via the postMessage method. This method adds a message to the other runtime if the latter listens to message events.

Asynchronous Primitives

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

Избежать блокирования позволяют различные методы асинхронного программирования и библиотеки:

  • Worker API - позволяет вынести длительные вычисления в отдельный параллельный процесс.

  • Asynchronous result - выполняя задачу, программист оставляет callback или другую функцию нотификации, которая будет вызвана когда задача завершится. Например, setTimeout это асинхронный sleep.

Существует два основных способа уведомления об асинхронном завершении задачи:

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

var req = new XMLHttpRequest();
req.open("GET", url);

req.onload = function() {
  if (req.status == 200) {
    processData(req.response);
  } else {
    console.log("ERROR", req.statusText);
  }
};

req.onerror = function() {
  console.log("Network Error");
};

req.send(); // Add request to task queue
  • Callbacks - это функции, которые передаются для вызова в том случае, когда выполнение асинхронной операции завершилось. Недостаком Callback-ов являются более сложные сигнатуры функций, более сложная обработка ошибок и усложненная композиция функций.

fs.readFile("myfile.txt", { encoding: "utf8" }, function(error, text) {
  // (A)
  if (error) {
    // ...
  }
  console.log(text);
});

Стиль программирования с использованием callback-ов часто называют CPS (continuation-passing style), поскольку следуший шаг выполнения явно передается в качестве параметра. Пример CPS:

console.log("A");
identity("B", function step2(result2) {
  console.log(result2);
  identity("C", function step3(result3) {
    console.log(result3);
  });
  console.log("D");
});
console.log("E");

// Output: A E B D C

function identity(input, callback) {
  setTimeout(function() {
    callback(input);
  }, 0);
}

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

  • Promise

  • Generators

  • Сопутствующие библиотеки

Таймеры

JS имеет таймеры вида: setTimeout(callback, ms). Функция setTimeout ожидает заданное число ms а затем добавлет задачу по вычислению callback в очередь. Стоит отметить, что ms регулирует значение когда задача именно добавляется в EL, а не выполняется в нем.

setTimeout(function() {
  // (A)
  console.log("Second");
}, 0);
console.log("First"); // (B)

First;
Second;

requestAnimationFrame Функция requestAnimationFrame позволяет координировать перерисовки в браузере.

Last updated