Галоўная / Артыкулы

Рефактарызацыя ланцаў Promise у async/Await без змены ўжывання

Як ператворыць ланцюгі .then() на чытальны код async/await, што на самай працэ павяршвае выконанне await, калі трэба запускаць крокі паралельна, і як try/catch заменяе .catch().

Выдавец: REACTAPP.TOP7 хв чытання1597 слоў

Крыніцы: Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Прамісы вывялі JavaScript з ситуацыі, калі было неабходна викорыстання вкладзеных калебрацый, але ланцюг залежных крокаў .then() яшчэ застаецца схожым на сложную сітку аперацый, а не на простую последоўнасць інструкцый. async/await закрывае гэтыя прыемнікі, дазволяючы асінхронным кодам выглядаць як звычны код, які выпалоўваецца зверху вярх, пры чым прамісы застаюцца ў сутнасі. У гэтым кяле показана, як безпечна пераканваты ланцюг прамісоў, што на самае працэ ўпынвае await, калі последоўныя вызовы await марнуюць час, і як змінюецца обробка адзінакоў, ўжыткам чаго можна перерабіць існуючы код без змены яго функцыянальнасці.

З ланцюга .then() да лінейной функцыі

Ёсьць типовы ланцюг, у яком кожны крок залежыць ад рэзультата пярэднего: спачатку заваносяцца даныя паўтаральніка, потым яго замовленні, пасля чаго — квітыцы на гэтыя замовленні, пры чым існуе адны калебратор для будзь-якіх адзінакоў.

getUser(id)
  .then(user => getOrders(user.id))
  .then(orders => getReceipts(orders))
  .then(receipts => {
    console.log(receipts);
  })
  .catch(err => handleError(err));

Это правільна практыка, і яна набагато кращая за викорыстанне вкладных калебэкаў, але вам все равно трэба візуальна праследаваць ланцюг, каб разумець перадачу данных. async/await, стандартызаваныя ў ES2017, былі створаныя, каб усунуць гэты последній атрыбут труднасці. Тая ж логіка, перапісаная, выглядае так:

async function loadData(id) {
  try {
    const user = await getUser(id);
    const orders = await getOrders(user.id);
    const receipts = await getReceipts(orders);
    console.log(receipts);
  } catch (err) {
    handleError(err);
  }
}

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

Завядомайце сабе адну важнаю рэч: async/await не заменяе прамісаў. Цэла яго сутнасць — сынтаксічны «цукор» над прамісамі. Прамісы як і раней виконваюць асінхронную работу; сынтаксіс толькі зменяе спосаб ўпісу і чытання.

Што зменяе ключавае слова async

Застосаванне async пры пачатку функцыі мае два адзінаковых эфекта. Функцыя, показаная нижэй, вяртае звычны об’ект:

async function getUser(id) {
  return { id, name: "Aarav" };
}

Яна завжды вяртае promise

Нават якщо getUser вяртае звычны об’ект, яго вызов дае promise, які пазначае гэты об’ект. Таму тыя, хто яго вызывае, можаць выкарыстоўваць яго як і будзь-яны іншы promise:

getUser(1).then(user => console.log(user));
// { id: 1, name: "Aarav" }

Якщо функцыя выклікае адказ, то вярнуты promise будзе адхіліць яго, замест таго каб адказ распрасіваўся сінхронна. Гэта лёгка забыць, калі функцыя, якая раней была сінхронной, стае async: кожны ты, хто раней выкарыстоўваў яе значэнне безпосередна, тепер атрымвае promise і ўсё яго трэба адкоректаваць.

Яна дазволяе выкарыстоўваць await у сваім тэле

await ўзьмліваець усередзіне async функцыі. Савэчасны JavaScript таксама дазваляе викорыстоўваць await на верхнім рэвэле ў ES-модулях, што яўляецца зручным для скрыптавання і ініцыялізаціі модуляў, але ў звычайных функцыях все рава патрэбна async.

Што на самай працы зупіняе await

Выраз await вертае кантроль назад пакуль його обяцанне не будзе рашытаянае. У разе успеху выраз выдае рашытуюе значэнне; у разе неудачы гэтае рэчыце вызывае абракцэс, так сама як і выраз throw. У прыкладзе логуецца паведамленне, чакаецца на корыстніка, а пасля — логуецца рэзультат:

async function loadUser() {
  console.log("Fetching user...");
  const user = await getUser(1); // pauses here until getUser's promise settles
  console.log("Got user:", user);
}

Расклад выходных дадзеных ў такім порядку:

Fetching user...
Got user: { id: 1, name: "Aarav" }

Важным моментам ёсць тое, што значыць „пауза“. Толькі гэтая функцыя зупіняецца, самэў той лініі. Раштка программы, уключаючы іншы код, таймеры і запыткі, якія прыходзяць на сервер, продовжае работаць. Калі обявленне стане вядомым, функцыя продовжае работу з таго месца, дзе зупінілася, пры чым значэнне, якое стало вядомым, прызначаецца зменнай.

Парадны выкарыстоўванне awaits і ўсё, што стоіць за ім

Калькі await паспелы працуюць адна за іншай:

async function loadDashboard(id) {
  const user = await getUser(id);
  const orders = await getOrders(user.id);
  const receipts = await getReceipts(orders);
  return receipts;
}

Кожная лінія чакае свайго крока, прычаму наступны можа запусціцца, што відтварае поведзенне ланцоўкі .then() без самай ланцоўкі. Гэта правільна, калі кожны крок патрабуе пылегледнага рэзультата, як у гэтым прыкладзе: нельга запрашваць замовленні, пакуль не будзе вядома інформацыя пра корыстніка.

Проблемы пачынаюцца, калі адны той самы стыль прыкладзяецца да операцый, якія не залежаць адзін ад другога. У наведаным выкладчыку панелі керування корыстнік, прыемленыя паведамленні і рэкамендацыі є незалежнымі, пры тым кожны запит чакае на паперадні:

async function loadDashboard() {
  const user = await getUser();
  const notifications = await getNotifications();
  const recommendations = await getRecommendations();

  return { user, notifications, recommendations };
}

Абсалютны час стае сумой ўсіх трох запытак, а не самага повольнага з іх.

Пачатак аднойчасовай роботы незалежных заданняў

Паколькі тры вызовы є незалежнымі, яны можна пачаць аднойчасова і чакаць на ўсе з іх за дапамой Promise.all():

async function loadDashboard() {
  const [user, notifications, recommendations] = await Promise.all([
    getUser(),
    getNotifications(),
    getRecommendations()
  ]);

  return { user, notifications, recommendations };
}

Усі тры запиты выконваюцца адночасова, і метод дэструктурызацыі дае рэзультаты ў там самым порядку, як і в масэ вхідных дадзеных. Важна знаты такое: Promise.all() выпалняецца толькі тады, калі всі прамісы выпаняюцца, а адмовляецца як толькі хоць адны з іх адмовяецца, не чакаючы рэшты. Якщо вы хочаце побачыць усі рэзультаты, незалежна ад неудач, тады Promise.allSettled() є кращым выборам. Аспекты, якія стосуюцца разліку межаў гэтых методаў, практычна расказаны ў выбор между Promise.all, Promise.race і sequential awaits.

Рэгуліраванне адзяйнаў за дапамойкай try і catch

За дапамойкай async/await звычны блок try/catch заменяе .catch(); такім чынам сінхронныя і асінхронныя неудачы нарэшце выкарыстоўваюць адну і тую ж структуру:

async function loadUser(id) {
  try {
    const user = await getUser(id);
    console.log(user);
  } catch (err) {
    console.log("Failed to load user:", err.message);
  }
}

Якщо запаведанне, якое вяртае getUser(id), абануецца, контроль пераходзіць безпосередня да блока catch, так сама, як і у разе памылкі, выкліканай сінхронным кодам. Заўважыце, што гэтая функцыя обрабоўвае памылку і пасля таго завершаецца нормальна, таму яе сопны запаведанне выпалняецца з значэннем undefined. Калекатары не можаць з’ясавіць, што завантажэнне не выйшла. Калі калекатару трэба реагаваць, неабходна перывыкнуць памылку пасля лагавання або зворачыць явны рэзультат.

Адні акцептар для кальколькох крокаў

Адзін блок try можа пакрыць цэлую серыю вызоваў await:

async function loadDashboard(id) {
  try {
    const user = await getUser(id);
    const orders = await getOrders(user.id);
    const receipts = await getReceipts(orders);
    return receipts;
  } catch (err) {
    console.log("Something failed in the dashboard load:", err.message);
  }
}

Як толькі яка-небудзь з крокаў выконання зазнае бяды, процес пераходзіць безпосередна да catch, таму не патрэбны аднальнікі для кожнага крока. Це адпавядае ефекту централізацыі, які мала .catch() у канцы ланцуга. Калі розныя крокі выконання патрабуюць разных способаў вярнення да нормальнага стану, напрыклад, толькі павтарэння запиту да квітанцыяў, трэба абгорнуць той крок у вялікішы try.

Порэванне обох стыляў

Ёсць той самы ладчык у обох форматах:

// Promises
function loadDashboard(id) {
  return getUser(id)
    .then(user => getOrders(user.id))
    .then(orders => getReceipts(orders))
    .catch(err => console.log("Failed:", err.message));
}

// async/await
async function loadDashboard(id) {
  try {
    const user = await getUser(id);
    const orders = await getOrders(user.id);
    return await getReceipts(orders);
  } catch (err) {
    console.log("Failed:", err.message);
  }
}

Ніяка з версій не зменяе асінхронную роботу падчынна. Выбар здабываецца на тым, якая з версій лёгча чытаема і падтрымвальна, а для логіки з колькома крокамі зазвычай выграўвае async/await. Адна тонкая деталь у другай версіі: return await getReceipts(orders) унутры try мае значэнне. Якшчы напісаць return getReceipts(orders) без await, то обявленне будзе вярнуцца раней, чым ён будзе завершаны, таму адхіленне з гэтага последнага крока будзе абходзіць блок catch цэлкам.

Частыя запытанні

Чы хоць await блакуе всю аплікацыю?

Нята. Ён толькі призупіняе асінхронную функцыю, якая яго мае. Іншы код, уключаючы іншы запытанні, продовжвае працаваць, пакуль гэтая функцыя чакае.

Можна вжываць await за межамі асінхронной функцыі?

Гэта не працюе ў звычных функціях. Аднак існуе аўтаматычнае перетварэнне await на вышэйшам рывеньце ў ES-модулях, якое дзейсніцца на рывеньце модуля, але не ўсередзіні звычных, ненявучых функцый.

Чы рэшт/await быстрей за promises?

Нята. Ён пабудаваны на promises, таму не можна спадзявацца на прыбутак у швальності ад змены синтаксісу. Прыбутак каментуецца у чытаемасці та адтыраноўнасці коду. Рэальныя прыбуткі ў швальності даходзяць ад аднойчаснага выканання незалежных задач, а не ад самай ключавой слова.

Што будзе без try/catch?

Якщо абсалютаваныя прабама, якая чакаецца, адхоўваеся і нічога яе не пераспрыямае, прабама самай асінхронной функцыі таксама адхоўваеца. Якщо ніхто, хто вызывае функцыю, таксама не распрацоўвае гэтае адхоўванне, сістэма запішвае пра нераспрацаванае адхоўванне, што ў ныякіх версіях Node.js за замовчаннем завершвае процес. Контролюйце адныя з бягункамі за дапамою try/catch унутры функцыі або за дапамою .catch() там, дзе яна вызываецца. Чырвоныя пра такія ситуацыі можна пазнакоміцца за лінкам пашыльныя хыбныя уявленні пра async/await, якія вызываюць бягункі ў прымененні.

Ключовыя выводы

  • async/await зміняе спосаб напісання асінхроннага коду, а не спосаб яго выканання; роботу продалей выкаńчваюць прабамы.
  • Асінхронная функцыя завжды вяртае прабаму, таму пераканвертаванне функцыі у async зміняе яе контракт для кожнага вызывальніка.
  • await адмініструе толькі свою сабэчную функцыю, а не цэлую праграму.
  • Крокі, якія выкарыстоўваюць await, выкананыя ў порядку, але незалежныя крокі трэба запускать разам з Promise.all() або Promise.allSettled().
  • try/catch навакол калькі await дапамагае цэнтрырацыяй обработкі адзінакоў; не забудзьце перрабіць адзінак, калі тым, хто вызывае код, трэба яго паведаміць, і выкорыстоўваць return await унутрані try.