Node.js Async Await Учебник – с примерами асинхронного JavaScript

1656050442 nodejs async await uchebnik – s primerami asinhronnogo javascript

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

асинхронный
Если вы впервые работали с асинхронностью не так, пожалуйста, считайте себя гением

Как бы трудно это ни было понять, асинхронное программирование имеет решающее значение для изучения, если вы хотите использовать JavaScript и Node.js для создания веб-приложений и серверов, поскольку код JS асинхронный по умолчанию.

Основы асинхронного программирования

Итак, что именно такое модель асинхронной обработки, или non-blocking I/O модель (о которой вы вероятно слышали, если вы пользователь Node.js)?

Вот описание TL;DR: в модели асинхронной обработки, когда ваш механизм программы взаимодействует с внешними сторонами (например, файловой системой или сетью), он не ожидает, пока получит результат от этих сторон. Вместо этого он продолжает выполнять следующие задачи и возвращается к предыдущим внешним сторонам только после получения сигнала о результате.

Чтобы понять модель асинхронной обработки по умолчанию в Node.js, посмотрим на гипотетическую мастерскую Деда Мороза. Прежде чем начать любую работу, Санта должна прочитать каждое из прекрасных писем от детей со всего мира.

Санта чтение письма для семинара

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

Санта передает инструкции красному

В этом году из-за пандемии COVID-19 только половина эльфов Деда Мороза может прийти в его мастерскую, чтобы помочь. Однако, поскольку он мудр, Санта решает, что вместо того, чтобы ждать, пока каждый эльф закончит готовить подарок (т.е. работать синхронно), он продолжит переводить и раздавать инструкции из своей груды писем.

Санта передает инструкции синему

Так далее и так далее…

Санта продолжает раздавать инструкции

Когда он только собирается прочесть еще одно письмо, Ред сообщает Санта, что он завершил
готовим первый подарок. Затем Санта получает подарок от Реда и откладывает его в сторону.

Санта получает красный подарок.

А затем продолжает переводить и передавать инструкции из следующего письма.

Санта передает инструкции зеленому

Поскольку ему нужно только завернуть готового летающего робота, Грин может быстро закончить приготовление и передать подарок Санти.

Санта получает подарок от Грина

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

Дед Мороз получил все подарки

Следовательно, это основная идея асинхронной или неблокирующей модели обработки ввода-вывода. Теперь давайте посмотрим, как это делается в Node.js конкретно.

Цикл событий Node.js

Возможно, вы слышали, что Node.js является однопоточным. Однако, если быть точным, только цикл событий в Node.js, взаимодействующий с пулом фоновых рабочих потоков C++, является однопоточным. В модели обработки Node.js есть четыре важных компонента:

  • Очередь событий: задачи, объявляемые в программе или возвращаемые из пула потоков обработки с помощью обратных вызовов. (Эквивалент этого в мастерской нашего Деда Мороза — куча писем для Санты.)
  • Цикл событий: основной поток Node.js, облегчающий очереди событий и пулы рабочих потоков для выполнения операций – как асинхронных, так и синхронных. (Это Санта. 🎅)
  • Фоновый пул потоков: эти потоки осуществляют фактическую обработку задач, которые
    может быть блокировка ввода-вывода (например, вызов и ожидание ответа от внешнего API). (Это трудолюбивые эльфы 🧝🧝‍♀️🧝‍♂️ из нашей мастерской.)

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

обработка-модель
Диаграмма предоставлена ​​c-sharpcorner.com

Давайте посмотрим на фактический фрагмент кода, чтобы увидеть их в действии:

console.log("Hello");
https.get(" (res) => {
  console.log(`API returned status: ${res.statusCode}`);
});
console.log("from the other side");

Если мы выполним приведенный выше фрагмент кода, мы получим это в стандартном выводе:

Hello
from the other side
API returned status: 200

Итак, как двигатель Node.js выполняет приведенный выше фрагмент кода? Он начинается с трех функций в стеке вызовов:

Обработка начинается с 3 функций в стеке вызовов

Затем Hello печатается на консоли, а соответствующий вызов функции удаляется из стека.

Журнал консоли Hello удален из стека

Вызов функции к https.get (т.е., запрашивающий получение соответствующего URL-адреса) затем выполняется и делегируется пула рабочих потоков с добавленным обратным вызовом.

https.get делегирован в пул работников

Следующий вызов функции к console.log выполняется, а «с другой стороны» выводится на консоль.

Далее будет выполнен console.log

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

Сетевой звонок завершен и обратный вызов поставлен в очередь

Затем обратный вызов попадает в наш стек вызовов:

Обратный вызов помещается в стек вызовов

а затем мы увидим «API возвращен статус: 200» в нашей консоли, например:

Распечатанный код состояния

Облегчая очередь обратных и стек вызовов, цикл событий в Node.js эффективно выполняет наш код JavaScript асинхронным способом.

Синхронная история JavaScript и Node.js async/await

Теперь, когда вы хорошо понимаете асинхронное выполнение и внутреннюю работу цикла событий Node.js, давайте погрузимся в async/await в JavaScript. Мы рассмотрим, как это работало с течением времени, от оригинальной реализации на основе обратного вызова до последних блестящих ключевых слов async/await.

Обратные вызовы в JavaScript

Способ OG обрабатывать асинхронную природу движков JavaScript заключался в обратных вызовах. Обратные вызовы – это в основном функции, которые будут выполняться, обычнов конце синхронных операций или операций блокировки ввода-вывода.

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

setTimeout(2000, () => {
  console.log("Hello");
});

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

  • Ад обратного звонка
  • Инверсия управления (не самый лучший!)

Что такое ад обратного звонка?

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

function translateLetter(letter, callback) {
  return setTimeout(2000, () => {
    callback(letter.split("").reverse().join(""));
  });
}
function assembleToy(instruction, callback) {
  return setTimeout(3000, () => {
    const toy = instruction.split("").reverse().join("");
    if (toy.includes("wooden")) {
      return callback(`polished ${toy}`);
    } else if (toy.includes("stuffed")) {
      return callback(`colorful ${toy}`);
    } else if (toy.includes("robotic")) {
      return callback(`flying ${toy}`);
    }
    callback(toy);
  });
}
function wrapPresent(toy, callback) {
  return setTimeout(1000, () => {
    callback(`wrapped ${toy}`);
  });
}

Эти действия необходимо выполнять в определенном порядке:

translateLetter("wooden truck", (instruction) => {
  assembleToy(instruction, (toy) => {
    wrapPresent(toy, console.log);
  });
});
// This will produced a "wrapped polished wooden truck" as the final result

Когда мы делаем это таким образом, добавление дополнительных шагов к процессу означает перемещение внутренних обратных вызовов вправо и в конечном итоге в аду обратного вызова вот так:

Ад обратного звонка

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

Итак, как вы решите эту проблему? Просто модулируйте вложенные обратные вызовы на именуемые функции, и вы получите хорошо выровненную программу по левому краю, которую легко читать.

function assembleCb(toy) {
  wrapPresent(toy, console.log);
}
function translateCb(instruction) {
  assembleToy(instruction, assembleCb);
}
translateLetter("wooden truck", translateCb);

Инверсия контроля

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

По сути, вы находитесь на милости владельцев ваших зависимостей, и вы никогда не знаете, когда они будут сломать ваш код.

Чтобы решить эту проблему, как пользователь зависимостей, вы ничего не можете с этим поделать. Однако, если вы когда-нибудь сами были владельцем зависимостей, всегда:

  • Соблюдайте обычную подпись обратного вызова с ошибкой как первым аргументом
  • Выполните обратный вызов только один раз в конце функции высшего порядка
  • Задокументируйте все, что не является обычным, что абсолютно необходимо, и всегда стремитесь к обратной совместимости

Обещания в JavaScript

Промесы были созданы для решения этих проблем с помощью обратных вызовов. Обещания гарантируют, что пользователи JavaScript:

  • Следуйте определенной конвенции с их подписью resolve и reject функции.
  • Связать функцию обратного вызова к четко выровненному потоку сверху вниз.

Наш предыдущий пример с готовящей подарки мастерской Деда Мороза можно переписать следующими обещаниями:

function translateLetter(letter) {
  return new Promise((resolve, reject) => {
    setTimeout(2000, () => {
      resolve(letter.split("").reverse().join(""));
    });
  });
}
function assembleToy(instruction) {
  return new Promise((resolve, reject) => {
    setTimeout(3000, () => {
      const toy = instruction.split("").reverse().join("");
      if (toy.includes("wooden")) {
        return resolve(`polished ${toy}`);
      } else if (toy.includes("stuffed")) {
        return resolve(`colorful ${toy}`);
      } else if (toy.includes("robotic")) {
        return resolve(`flying ${toy}`);
      }
      resolve(toy);
    });
  });
}
function wrapPresent(toy) {
  return new Promise((resolve, reject) => {
    setTimeout(1000, () => {
      resolve(`wrapped ${toy}`);
    });
  });
}

при этом шаги выполняются аккуратно по цепочке:

translateLetter("wooden truck")
  .then((instruction) => {
    return assembleToy(instruction);
  })
  .then((toy) => {
    return wrapPresent(toy);
  })
  .then(console.log);
// This would produce the exact same present: wrapped polished wooden truck

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

К примеру, наш этап упаковки подарков может захотеть использовать данные с этапа перевода:

function wrapPresent(toy, instruction) {
  return Promise((resolve, reject) => {
    setTimeout(1000, () => {
      resolve(`wrapped ${toy} with instruction: "${instruction}`);
    });
  });
}

Это скорее классическая проблема «раздела памяти» с потоками. Чтобы решить эту проблему, вместо использования переменных в родительской области, мы должны использовать Promise.all и «обмениваться данными путем общения, а не общаться путем обмена данными».

translateLetter("wooden truck")
  .then((instruction) => {
    return Promise.all([assembleToy(instruction), instruction]);
  })
  .then((toy, instruction) => {
    return wrapPresent(toy, instruction);
  })
  .then(console.log);
// This would produce the present: wrapped polished wooden truck with instruction: "kcurt nedoow"

Async/Await в JavaScript

И последнее, но не менее важное, самый яркий ребенок в квартале – это async/await. Он очень прост в использовании, но у него также есть определенные риски.

Async/await решает проблемы совместного использования памяти промисов, имея все в одной области. Наш предыдущий пример можно легко переписать так:

(async function main() {
  const instruction = await translateLetter("wooden truck");
  const toy = await assembleToy(instruction);
  const present = await wrapPresent(toy, instruction);
  console.log(present);
})();
// This would produce the present: wrapped polished wooden truck with instruction: "kcurt nedoow"

Однако, насколько легко писать асинхронный код с помощью async/await, также легко совершать ошибки, создающие лазейки в производительности.

Давайте теперь локализуем наш пример сценария мастерской Деда Мороза на упаковку подарков и погрузку их на сани.

function wrapPresent(toy) {
  return Promise((resolve, reject) => {
    setTimeout(5000 * Math.random(), () => {
      resolve(`wrapped ${toy}`);
    });
  });
}
function loadPresents(presents) {
  return Promise((resolve, reject) => {
    setTimeout(5000, () => {
      let itemList = "";
      for (let i = 0; i < presents.length; i++) {
        itemList += `${i}. ${presents[i]}\n`;
      }
    });
  });
}

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

(async function main() {
  const presents = [];
  presents.push(await wrapPresent("wooden truck"));
  presents.push(await wrapPresent("flying robot"));
  presents.push(await wrapPresent("stuffed elephant"));
  const itemList = await loadPresents(presents);
  console.log(itemList);
})();

Но нужно ли Санты await чтобы каждый из подарков был завернут по одному перед загрузкой? Точно нет! Подарки должны быть обернуты одновременно. Вы можете часто совершать эту ошибку, поскольку это так легко писать await не задумываясь о блокирующем характере ключевого слова.

Чтобы решить эту проблему, мы должны объединить этапы упаковки подарков вместе и выполнить их все одновременно:

(async function main() {
  const presents = await Promise.all([
    wrapPresent("wooden truck"),
    wrapPresent("flying robot"),
    wrapPresent("stuffed elephant"),
  ]);
  const itemList = await loadPresents(presents);
  console.log(itemList);
})();

Ниже приведены несколько рекомендуемых шагов для решения проблемы с производительностью параллельности в коде Node.js:

  • Определите горячие точки с несколькими последовательными ожиданиями в вашем коде
  • Проверьте, зависят ли они друг от друга (т.е. одна функция использует данные, возвращенные с другой)
  • Сделайте независимые вызовы функций одновременно с Promise.all

Подведение итогов (статья, а не рождественские подарки 😂)

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

Вот несколько ключевых выводов:

  • Модулируйте свои обратные вызовы JavaScript, чтобы избежать ада обратных вызовов
  • Соблюдайте конвенцию об обратных вызовах JS
  • Делитесь данными, общаясь через Promise.all при использовании обещаний
  • Будьте осторожны относительно производительности кода async/await

Мы ❤️ JavaScript 🙂

Спасибо, что читаете!

И последнее, но не менее важное: если вам нравятся мои произведения, пожалуйста, перейдите к моему блогу, чтобы получить подобные комментарии и следите за мной в Twitter. 🎉

Добавить комментарий

Ваш адрес email не будет опубликован.