Promises, async/await, and the event loop. 

Agent execution model

В спецификации ECMAScript Agent — это самостоятельный исполнитель JavaScript-кода.

 Каждый агент работает в одном потоке и имеет свой собственный стек вызовов (Execution Context Stack) и очередь задач (Job Queue).

 

Он может быть:

  • Веб-страницей в браузере

  • Web Worker-ом

  • Node.js процессом

Каждый агент имеет свой собственный Realm, который состоит из такой информации, как, например,

глобальный объект список прототипов и глобальные переменные

Кроме того, каждый агент управляет своими собственными:

 

Heap (of objects)

 

Queue (of jobs)

 

Stack (of execution contexts)

 

 

в HTML (и других языках) это и называется event loop, который позволяет асинхронно программировать в однопоточном

JavaScript

  •  Microtask Queue — очередь микрозадач:

    • Promise.then/catch/finally

    • Функции, добавленные через queueMicrotask()

    • MutationObserver

 

  • Task Queue— крупные асинхронные задачи, как:

    • setTimeout, setInterval,

    • События DOM и их обработчики

Jobs

  • JavaScript — однопоточный

  • JavaScript выполняет код, управляя его контекстом выполнения и стеком вызовов

  • Асинхронность достигается через Event Loop (события) и очереди задач

  • Нет sleep, встроенной функции, которая просто "приостанавливает" выполнение кода на какое-то время

Найдите неверное утверждение:​

Все верные!

PROMISE

  • Объект, представляющий результат асинхронной операции.
  • Объект, реализующий Thenable интерфейс.

  • Имеет 3 состояния: pending, fulfilled, rejected.

 

 

 

 

 

  • Иммутабелен после перехода в fulfilled или rejected.

THENABLE

Это любой объект с методом then(), но не обязательно поддерживающий внутреннее состояние или спецификацию, как у промиса.

Может не поддерживать правильную логику для обработки состояний или асинхронных операций.

const notARealPromise = {
  then: function(resolve, reject) {
    // Не вызывает resolve или reject
    console.log("Then called, but no resolve/reject");
  }
};

notARealPromise.then(console.log);  // Output: "Then called, but no resolve/reject"

Внутренние свойства промиса

  • [[PromiseState]]: "pending", "fulfilled", "rejected"

  • [[PromiseResult]]: значение или ошибка

  • [[PromiseFulfillReactions]] очередь реакций на успешное выполнение / [[PromiseRejectReactions]] очередь реакций на отклонение

  • [[PromiseIsHandled]] для трекинга unhandled rejection

Reactions

let promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("Success!"), 1000);  // Промис будет разрешён через 1 секунду
});

// Первая реакция
promise.then(result => {
  console.log("First reaction:", result);  // добавляется в `[[PromiseFulfillReactions]]`
});

// Вторая реакция
promise.then(result => {
  console.log("Second reaction:", result);  //  добавляется в `[[PromiseFulfillReactions]]`
});

 

Когда промис становится разрешённым (fulfilled) или отклонённым (rejected), его колбэки попадают в очередь реакций ([[PromiseFulfillReactions]] или [[PromiseRejectReactions]]).

let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("Failure!");  // Промис будет отклонён (rejected) через 1 секунду
  }, 1000);
});

// Первая реакция на отклонение (попадает в `[[PromiseRejectReactions]]`)
promise
  .catch(error => {
    console.log("First rejected reaction:", error); 
  });


// Через второй аргумент в `.then()` вторая реакция (попадает в `[[PromiseRejectReactions]]`)
promise
  .then(
    result => {
      console.log("This will never log, as the promise is rejected.");
    },
    error => {
      console.log("Second rejected reaction:", error);  // попадает в `[[PromiseRejectReactions]]`
    }
  );

// Третья реакция на отклонение (попадает в `[[PromiseRejectReactions]]`)
promise
  .catch(error => {
    console.log("Third rejected reaction:", error); 
  });

 

  • Эти колбэки не выполняются сразу. Они ставятся в очередь микротасков (microtask queue), а не в очередь обычных событий (таких как setTimeout).

  • Обработка микротасков происходит после завершения текущего синхронного кода, но до выполнения любых других асинхронных операций, таких как setTimeout, setInterval или другие..

Promise Resolution Procedure

Рекурсивная распаковка thenable-объектов

let promise1 = new Promise((resolve) => {
  resolve(new Promise((resolve) => {
    resolve("Пример");
  }));
});

promise1.then(result => {
  console.log(result); // "Пример"
});

Когда промис вызывает .then(), и результатом является другой промис:

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

Циклические ссылки → TypeError

let promise1 = new Promise((resolve) => {
  resolve(promise1);  // Промис ссылается сам на себя
});

promise1.then(result => {
  console.log(result);
}).catch(error => {
  console.log(error); // TypeError: Chaining cycle detected for promise
});
  • Здесь промис promise1 ссылается на сам себя, что создает циклическую зависимость.

  • При попытке разрешить такой промис, возникает ошибка TypeError: Chaining cycle detected for promise.

  • Это предотвращает зацикливание в цепочке промисов.

then/catch/finally

  • Все методы возвращают новый Promise

  • then(onFulfilled, onRejected)

  • catch(fn) = then(null, fn)

  • finally(fn) вызывается всегда, но не меняет результат

then

Метод then используется для обработки успешного завершения промиса. Он принимает два аргумента:

  • Первый аргумент (обязателен): функция, которая будет вызвана, когда промис разрешится (т.е. когда его статус станет fulfilled).
  • Второй аргумент (необязателен): функция, которая будет вызвана, если промис будет отклонен (т.е. когда его статус станет rejected).

catch

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

Важно, что catch будет ловить любые ошибки, если они не были обработаны внутри промиса через второй аргумент then

new Promise((_, reject) => reject('Ошибка'))
  .catch(error => {
    console.log(error); // 'Ошибка'
  });

finally

Метод finally выполняет код, который должен быть выполнен в любом случае, независимо от того, был ли промис разрешен или отклонен. Это полезно для выполнения действий, которые должны происходить всегда (например, очистка ресурсов, закрытие соединений и т. д.).

async/await

async/await — это синтаксический сахар над промисами. С ними асинхронный код выглядит как синхронный, что делает его понятнее и читаемее, официально добавлены в ECMAScript 2017 (ES8).

Promise.resolve(10).then(value => {
  console.log(value); // 10
});



async function bar() {
  let value = await Promise.resolve(10);
  console.log(value); // 10
}

async-функция возвращает Promise

Любая функция, помеченная как async, всегда возвращает промис. Даже если в ней не используется await, она все равно будет возвращать промис

async function myFunction() {
  return 42;  // Это автоматически будет wrapped в Promise
}

myFunction().then(console.log);  // Выведет 42

await — ждет результат (или ошибку)

Ключевое слово await используется внутри async-функции и ожидает разрешения (или отклонения) промиса. Он "приостанавливает" выполнение текущей async-функции до получения результата.

Под капотом — то же, что и then, но с паузой в стейте выполнения
Внутри async/await все работает через промисы, просто синтаксис делает его более читаемым и последовательным. Использование await заменяет цепочку .then(), делая код более похожим на синхронный, но асинхронный.

async function example() {
  const result = await new Promise(resolve => setTimeout(() => resolve('Привет!'), 1000));
  console.log(result);  // Выведет 'Привет!' через 1 секунду
}
example();

Как работает async/await (внутренне)

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

  • На каждой итерации await:

  1. Выполнение останавливается на моменте await до тех пор, пока промис не будет разрешен или отклонен.
  2. Возвращается Promise, который будет разрешен значением или ошибкой.
  3. После resolve — продолжение исполнения функции, начиная с того места, где выполнение было приостановлено.

Awaitable values

Что может быть передано в await:

  • Promise — будет ждать разрешения или отклонения.

  • Любой thenable — объект, у которого есть метод .then(). Это может быть не только объект Promise, но и любой объект, реализующий интерфейс thenable.

  • Обычное значение — сразу возвращает значение без ожидания.

//Внутри await, движок делает что-то вроде:

Promise.resolve(значение)

await 42

/// ====>

await Promise.resolve(42)
  • Если значение — обычное (например, 42), оно сразу возвращается, но await всё равно делает паузу до следующей микротаски.

Методы Promise

Поведение в цикле

Последовательные запросы

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

  • Минусы:

    1. Медленно. Все запросы выполняются один за другим.

    2. Неэффективно. Блокировка на каждом запросе.

for (const url of urls) {
  await fetch(url); // последовательно!
}

Параллельные запросы

  • Как работает: Все запросы отправляются одновременно, Promise.all ждёт их завершения

await Promise.all(urls.map(fetch));
// параллельно
  • Преимущества:

    1. Быстрее. Все запросы выполняются одновременно.

    2. Эффективно. Максимальное использование ресурсов сети и сервера.

  • Минусы:

    1. Могут возникнуть проблемы с ресурсами при слишком большом количестве параллельных запросов.

    2. Ошибки в одном запросе прерывают всю операцию (нужна обработка ошибок).

Promise нельзя отменить

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

Использование AbortController

 

 

  • Это объект, который позволяет отменить асинхронную операцию, такую как запросы с fetch.

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

const controller = new AbortController();
const signal = controller.signal;

fetch(url, { signal }) // Передаем signal в fetch для отслеживания отмены
  .then(response => {
    // обработка ответа
  })
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Запрос был отменен');
    }
  });

// Отмена запроса
controller.abort();

Сancellable wrapper

Обертка для промиса, которая будет отслеживать отмену через флаг или сигнал.

class CancellablePromise {
  constructor(executor) {
    this.cancelled = false;
    this.promise = new Promise((resolve, reject) => {
      executor(resolve, reject);
    });
  }

  cancel() {
    this.cancelled = true;
  }

  then(onFulfilled, onRejected) {
    return this.promise.then(
      value => (this.cancelled ? Promise.reject('Cancelled') : onFulfilled(value)),
      onRejected
    );
  }
}

// Пример использования:
const cancelable = new CancellablePromise((resolve, reject) => {
  setTimeout(() => resolve('Done'), 2000);
});

cancelable.then(result => console.log(result));
cancelable.cancel(); // Останавливает выполнение

Unhandled Rejections

Спецификация JavaScript не предоставляет стандартного способа обработки необработанных отклонений промисов. То, как эти отклонения обрабатываются, зависит от среды выполнения (например, Node.js или браузер).

Node.js

  • Ранее: В более ранних версиях Node.js необработанное отклонение промиса приводило к крашу процесса.

  • Сейчас: Node.js генерирует предупреждение, если промис отклоняется, но его ошибка не обрабатывается (например, через .catch() или try-catch).

process.on('unhandledRejection', (reason, promise) => {
  console.log('Необработанное отклонение промиса:', reason);
  // Здесь можно предпринять действия, например, логировать ошибку
});

Браузеры

  • Chrome, Firefox, Safari, Edge поддерживают window.onunhandledrejection для глобальной обработки необработанных отклонений.

window.onunhandledrejection = (event) => {
  console.log('Необработанное отклонение промиса:', event.reason);
  // Здесь можно выполнить действия, например, логировать ошибку
};
Made with Slides.com