تکامل جاوااسکریپت ناهمگام: از callbackها، تا promiseها و Async / Await - بخش دوم

13 خرداد 1398, خواندن در 20 دقیقه

در بخش اول این مقاله، callbackها را مورد بررسی قرار دادیم و در این بخش به promiseها و Async / Await خواهیم پرداخت. با ما همراه باشید...

Promiseها

آیا تا به حال بدون رزرو کردن به رستوران شلوغی رفته‌اید؟ وقتی که این اتفاق می‌افتد، رستوران به راهی نیاز دارد تا وقتی یک میز خالی می‌شود با شما تماس حاصل کند. از نظر تاریخی، آن‌ها فقط نام شما را می‌گیرند و وقتی که میزتان آماده است، آن را صدا می‌زنند. سپس، آن‌ها تصمیم گرفتند فانتزی‌تر شوند. یک راه حل این بود که به جای گرفتن نام شما، آن‌ها شماره تلفن شما را بگیرند و وقتی میزی خالی می‌شود، به شما پیامک بدهند. این مسئله شما را قادر ساخت تا خارج از محدوده فریاد زدن باشید، اما مهم‌تر این که آن‌ها را قادر ساخت تا تلفن شما را تحت هدف هر تبلیغاتی که می‌خواستند قرار دهند.

آیا آشنا به نظر می‌رسد؟ باید هم همینطور باشد. خب، شاید هم اینطور نیست. این استعاره‌ای برای callbackها است. دادن شماره تلفن خود به یک رستوران، درست به مانند دادن یک تابع callback به یک سرویس جداگانه است. شما انتظار دارید وقتی که یک میز خالی می‌شود، رستوران به شما پیامک دهد؛ همانطور که انتظار دارید آن سرویس همانطور که گفت است، تابع شما را فراخوانی کند. گرچه وقتی که شماره تلفن یا تابع callback شما در دستان آن‌ها است، شما کنترل را از دست داده‌اید.

خوشبختانه یک راه حل دیگر هم وجود دارد. یک راه حل که شما را قادر می‌سازد تا کنترل را در دستان خود نگه دارید. احتمالا آن را قبلا هم تجربه کرده‌اید. همان دکمه زنگی که رستوران‌ها به شما می‌دهند.

اگر تا به حال از آن‌ها استفاده نکرده‌اید، ایده آن‌ها ساده است. به جای گرفتن نام و شماره تلفن شما، آن‌ها این دستگاه را به شما می‌دهند. وقتی که دستگاه شروع به ویبره و درخشش می‌کند، میز شما آماده است. شما همینطور که منتظر خالی شدن میز خود هستید، همچنان می‌توانید هر کاری که دوست دارید انجام دهید، اما حال شما چیزی را از دست نمی‌دهید. در واقع، کاملا عکس آن صحیح است. آن‌ها مجبورند که چیزی به شما بدهند. هیچ معکوس کردنی در کنترل وجود ندارد.

این دستگاه همیشه در یکی از سه حالت مختلف خواهد بود: pending، fulfilled یا rejected.

Pending همان وضعیت پیشفرض و اولیه است. وقتی که دستگاه را به شما می‌دهند، در این حالت قرار دارد.

Fuilfilled وضعیتی است که دستگاه به شما نشان می‌دهند که میزتان آماده است.

Rejected وضعیتی است که وقتی چیزی اشتباه پیش می‌رود، دستگاه به آن وضعیت می‌رود. شاید رستوران قرار است به زودی بسته شود یا این که آن‌ها فراموش کردند شخصی رستوران را برای یک شب رزرو کرده بود.

باز هم مسئله مهم که باید به یاد داشته باشید این است که شما، یعنی دریافت کننده دستگاه، تمام کنترل را در دست دارید. اگر دستگاه به حالت fulfilled برود، شما می‌توانید به میز خود بروید. اگر به حالت fulfilled برود و شما بخواهید آن را نادیده بگیرید، مشکلی ندارد؛ می‌توانید این کار را انجام دهید. اگر به حالت rejected برود، این اتفاق بدی است، اما شما می‌توانید به مکانی دیگر برای صرف غذا بروید. اگر هیچ اتفاقی نیفتد و دستگاه در حالت pending بماند، شما هیچ وقت نمی‌توانید غذایی صرف کنید، اما از هیچ چیز خارج نیستید.

حال که شما سرور دستگاه رستوران هستید، بیایید این دانش را به چیز دیگری صدق دهیم که اهمیت دارد.

اگر دادن شماره تلفن خود به رستوران، مانند دادن یک تابع callback به آن‌ها است، دریافت آن دستگاه هم چیزی به نام «promise» می‌باشد.

به مانند همیشه، بیایید با «چرا؟» شروع کنیم. چرا promiseها وجود دارند؟ promiseها وجود دارند تا پیچیدگی درخواست‌های ناهمگام را قابل مدیریت‌تر کنند. درست به مانند یک دستگاه در رستوران، یک promise می‌تواند در سه حالت باشد: pending، fulfilled یا rejected. بر خلاف دستگاه در رستوران، به جای این که این stateها وضعیت یک میز در یک رستوران را نمایش دهند، در واقع وضعیت یک درخواست ناهمگام را نشان می‌دهند.

اگر درخواست ناهمگام همچنان پابرجاست، promise مورد نظر وضعیت pending را خواهد داشت. اگر درخواست ناهمگام با موفقیت انجام شده بود، promise مورد نظر به وضعیت fulfilled تغییر خواهد کرد. اگر درخواست ناهمگام با شکست مواجه شود، promise مورد نظر  به وضعیت rejected تغییر خواهد کرد.

حال که علت وجود promiseها و وضعیت‌های مختلفی که می‌توانند در آن‌ها باشند را درک می‌کنید، سه سوال دیگر هستند که باید پاسخ دهیم.

۱. چگونه باید یک promise را ساخت؟

۲. چگونه وضعیت یک promise را باید تغییر داد؟

۳. چگونه به یک تغییر وضعیت promise گوش می‌دهید؟

۱) چگونه باید یک promise را ساخت؟

این سوال بسیار ساده است. شما می‌توانید یک نمونه جدید (new) از یک promise را بسازید.

const promise = new Promise()

۲) چگونه وضعیت یک promise را باید تغییر داد؟

تابع سازنده promise یک آرگومان تکی، یعنی یک تابع callback را می‌گیرد. قرار است دو آرگومان به این تابع منتقل شوند: resolve و reject.

Resolve - یک تابع که شما را قادر می‌سازد تا وضعیت یک promise را به fulfilled تغییر دهید.

Reject - یک تابع که شما را قادر می‌سازد تا وضعیت یک promise را به rejected تغییر دهید.

در کد زیر، ما از setTimeout استفاده می‌کنیم تا ۲ ثانیه صبر کنیم و سپس resolve را فراخوانی کنیم. این کار وضعیت promise را به fulfilled تغییر خواهد داد.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve() // Change status to 'fulfilled'
  }, 2000)
})

ما می‌توانیم با log کردن promise مورد نظر درست بعد از این که آن را ساختیم، و باز هم ۲ ثانیه بعد از این که resolve فراخوانی شده است، این تغییر را در عمل ببینیم.

دقت کنید که promise مورد نظر از <pending> به <resolved> می‌رود.

۳) چگونه به یک تغییر وضعیت promise گوش می‌دهید؟

از نظر من این مهم‌ترین سوال است. این که ما می‌دانیم چگونه یک promise را بسازیم و وضعیت آن را تغییر دهیم خوب است، اما اگر نمی‌دانیم که پس از تغییر وضعیت چگونه کاری انجام دهیم، ارزشی ندارد.

یک مسئله که هنوز درباره‌اش صحبت نکرده‌ایم، این است که یک promise در واقع چیست. وقتی که شما یک promise جدید می‌سازید، شما در واقع یک آبجکت JavaScript می‌سازید. این آبجکت می‌تواند دو متد را فراخوانی کند: then و catch. کلید در اینجاست. وقتی که وضعیت promise به fulfilled تغییر می‌کند، تابعی که به .then منتقل شده است فراخوانی خواهد شد. وقتی که وضعیت promise به rejected تغییر می‌کند، تابعی که به .catch منتقل شده است فراخوانی خواهد شد. معنای آن این است که وقتی شما یک promise را می‌سازید، شما تابعی که می‌خواهید اگر درخواست ناهمگام با موفیقیت انجام شده است، اجرا شود را به .then منتقل می‌کنید، و تابعی که می‌خواهید اگر درخواست ناهمگام با موفیقیت انجام نشده است، اجرا شود را به .catch منتقل می‌کنید.

بیایید به یک مثال نگاهی داشته باشید. ما باز هم از setTimeout استفاده خواهیم کرد تا وضعیت promise را پس از ۲ ثانیه (۲۰۰۰ میلی ثانیه) به fulfilled تغییر دهیم.

function onSuccess () {
  console.log('Success!')
}

function onError () {
  console.log('?')
}

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 2000)
})

promise.then(onSuccess)
promise.catch(onError)

اگر کد بالا را اجرا کنید، متوجه خواهید شد که ۲ ثانیه بعد، «Success!» را در کنسول خواهید دید. باز هم علت این اتفاق دو چیز است. اول، وقتی که ما promise را ساختیم، پس از ۲۰۰۰ میلی ثانیه resolve را فراخوانی کردیم. این مسئله وضعیت promise را به fulfilled تغییر داد. دوم، ما تابع onSuccess را به متد .then مربوط به promise منتقل کردیم. ما با انجام این کار به promise گفتیم که وقتی که وضعیت آن به fulfilled تغییر کرده است، که پس از ۲۰۰۰ میلی ثانیه اتفاق افتاد، onSuccess را فراخوانی کند.

حال بیایید وانمود کنیم که اتفاق بدی افتاده است و ما می‌خواهیم وضعیت promise را به rejected تغییر دهیم. ما به جای فراخوانی resolve، در واقع reject را فراخوانی خواهیم کرد.

function onSuccess () {
  console.log('Success!')
}

function onError () {
  console.log('?')
}

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject()
  }, 2000)
})

promise.then(onSuccess)
promise.catch(onError)

حال این بار به جای این که تابع onSuccess فراخوانی شود، تابع onError فراخوانی خواهد شد؛ زیرا ما reject را فراخوانی کردیم.

حال که نحوه کار اِی‌پی‌آی promise را می‌دانید، بیایید به یک کد واقعی نگاهی داشته باشیم.

آیا مثال callback ناهمگام آخر که پیش‌تر دیدیم را به یاد دارید؟

function getUser(id, onSuccess, onFailure) {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: onSuccess,
    error: onFailure
  })
}

function getWeather(user, onSuccess, onFailure) {
  $.getJSON({
    url: getLocationURL(user.location.split(',')),
    success: onSuccess,
    error: onFailure,
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis", (user) => {
    getWeather(user, (weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    }, showError)
  }, showError)
})

آیا راهی وجود دارد که ما در اینجا به جای اِی‌پی‌آی promise از callbackها استفاده کنیم؟ اگر درخواست AJAX خود را داخل یک promise جمع‌بندی کنیم چه؟ در آن صورت ما به سادگی بر پایه نحوه کار promise مورد نظر، resolve یا reject می‌کنیم. بیایید با getUser شروع کنیم.

function getUser(id) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: `https://api.github.com/users/${id}`,
      success: resolve,
      error: reject
    })
  })
}

خب. دقت کنید که پارامترهای getUser تغییر کرده‌اند. این تابع به جای دریافت id، onSuccess و onFailure، فقط id را دریافت می‌کند. نیازی به آن دو تابع callback دیگر نیست؛ زیرا ما دیگر کنترل را معکوس نمی‌کنیم. در عوض، ما از توابع resolve و reject مربوط به promise استفاده می‌کنید. Resolve اگر درخواست موفق بوده باشد فراخوانی خواهد شد، و reject اگر خطایی وجود داشته باشد فراخوانی خواهد شد.

سپس بیایید getWeather را بازسازی کنیم. ما در اینجا از استراتژی مشابهی پیروی خواهیم کرد. به جای گرفتن توابع onSuccess و onFailure، ما از resolve و reject استفاده خواهیم کرد.

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(',')),
      success: resolve,
      error: reject,
    })
  })
}

این کد خوب به نظر می‌آید. حال آخرین چیزی که باید بروزرسانی کنیم، handler کلیک ما است. به یاد داشته باشید، این جریانی است که با آن پیش خواهیم رفت:

۱. اطلاعات کاربر را از اِی‌پی‌آی Github بگیریم.

۲. از موقعیت مکانی کاربر برای گرفتن آب و هوای آن‌ها از ِی‌پی‌آی Yahoo Weather استفاده کنیم.

۳. رابط کاربری را با اطلاعات کاربر و آب و هوای آن‌ها بروزرسانی کنیم.

بیایید با مورد اول شروع کنیم: دریافت اطلاعات کاربر از اِی‌پی‌آی Github.

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

userPromise.then((user) => {

})

userPromise.catch(showError)
})

دقت کنید که به جای این که getUser دو تابع callback را بگیرد، یک promise را به ما بر می‌گرداند که می‌توانیم .then و .catch را بر رویش فراخوانی کنیم. اگر .then فراخوانی شود، با اطلاعات کاربر فراخوانی خواهد شد. اگر .catch فراخوانی شود، با خطا فراخوانی خواهد شد.

حال بیایید مورد دوم را انجام دهیم: از موقعیت مکانی کاربر استفاده کنیم تا آب و هوای او را بگیریم.

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

userPromise.then((user) => {
    const weatherPromise = getWeather(user)
    weatherPromise.then((weather) => {

})

weatherPromise.catch(showError)
  })

userPromise.catch(showError)
})

دقت کنید که دقیقا الگویی که در مورد اول دنبال کردیم را دنبال می‌کنیم، اما حال با انتقال آبجکت user که از userPromise گرفتیم به userPromise، آن را فراخوانی می‌کنیم.

در آخر هم مورد سوم: بروزرسانی رابط کاربری با اطلاعات کاربر و آب و هوای او.

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

userPromise.then((user) => {
    const weatherPromise = getWeather(user)
    weatherPromise.then((weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    })

weatherPromise.catch(showError)
  })

userPromise.catch(showError)
})

کد کامل را می‌توانید در این لینک پیدا کنید و با آن کار کنید.

کد جدید ما بهتر است، اما هنوز هم برخی پیشرفت‌ها وجود دارند که می‌توانیم به آن بیاوریم. گرچه قبل از این که بتوانیم آن ارتقاها را انجام دهیم، دو امکان دیگر از promiseها وجود دارند که باید از آن‌ها آگاه باشید: زنجیر کردن و منتقل کردن آرگومان‌ها از resolve به then.

زنجیر کردن (chaining)

هم .then و هم .catch یک promise جدید را باز خواهند گرداند. این به نظر یک جزئیات کوچک می‌آید، اما بسیار مهم است؛ زیرا معنای آن این است که آن promise می‌تواند زنجیر شود.

در مثال زیر، ما getPromise را فراخوانی می‌کنیم که یک promise را به ما بر می‌گرداند، و این promise در حداقل ۲۰۰۰ میلی ثانیه resolve خواهد شد. با توجه به این که .then یک promise را بر خواهد گرداند، ما می‌توانیم از آنجا .then های خود را با هم زنجیر کنیم، تا زمانی که یک خطای جدید را ارسال کنیم، که توسط متد .catch دریافت می‌شود.

function getPromise () {
  return new Promise((resolve) => {
    setTimeout(resolve, 2000)
  })
}

function logA () {
  console.log('A')
}

function logB () {
  console.log('B')
}

function logCAndThrow () {
  console.log('C')

throw new Error()
}

function catchError () {
  console.log('Error!')
}

getPromise()
  .then(logA) // A
  .then(logB) // B
  .then(logCAndThrow) // C
  .catch(catchError) // Error!

خب، اما چرا این مسئله اینقدر مهم است؟ به یاد بیاورید که پیش‌تر و در بخش callback، ما درباره یکی از نکات منفی callbackها صحبت کردیم، و آن نکته منفی این بود که callbackها شما را مجبور می‌کنند تا از طرز تفکر طبیعی و ترتیبی خود خارج شوید. وقتی که شما promiseها را با یکدیگر زنجیر می‌کنید، شما را مجبور نمی‌کننند که از طرز تفکر طبیعی خود خارج شوید؛ زیرا promiseها به ترتیب هستند. getPromise اجرا می‌شود، سپس logA اجرا می‌شود، سپس logB اجرا می‌شود و...

فقط برای این که بتوانید یک مثال دیگر ببینید، در اینجا یک موقعیت استفاده رایج را مشاهده می‌نمایید که از اِی‌پی‌آی fetch استفاده می‌کنیم. Fetch‌ یک promise را برای شما باز خواهد گرداند که با پاسخ HTTP رفع خواهد شد. برای دریافت JSON اصلی، شما باید .json را فراخوانی کنید. با توجه به زنجیر کردن، ما می‌توانیم درباره این مسئله به روشی ترتیبی فکر کنیم.

fetch('/api/user.json')
  .then((response) => response.json())
  .then((user) => {
    // یک کاربر جدید آماده کار است
  })

حال که ما زنجیر کردن را می‌دانیم، بیایید کد getUser / getWeather خود را بازسازی کنیم و از آن استفاده کنیم.

function getUser(id) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: `https://api.github.com/users/${id}`,
      success: resolve,
      error: reject
    })
  })
}

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(',')),
      success: resolve,
      error: reject,
    })
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((weather) => {
      // در اینجا هم به کاربر و هم به آب و هوا نیاز داریم
      // در حال حاضر فقط آب و هوا را داریم
      updateUI() // ????
    })
    .catch(showError)
})

حال این کد بسیار بهتر به نظر می‌رسد، اما به یک مشکل برخورده‌ایم. آیا می‌توانید آن را ببینید؟ ما در .then دوم می‌خواهیم updateUI را فراخوانی کنیم. مشکل این است که باید هم user و هم weather را به updateUI منتقل کنیم. به صورتی که در حال حاضر ما آن را راه‌اندازی کرده‌ایم، فقط weather را دریافت می‌کنیم، نه user را. به نوعی باید راهی پیدا کنیم تا promiseای که getWeather بر می‌گرداند، هم با user و هم با weather رفع شود.

کلید در اینجاست. Resolve فقط یک تابع است. هر آرگومانی که شما به آن منتقل کنید، به همراه تابع داده شده به .then منتقل خواهد شد. معنای آن این است که داخل getWeather، اگر ما خودمان resolve را فراخوانی کنیم، می‌توانیم weather و user را به آن منتقل کنیم. سپس متد .then دوم در زنجیر ما، هم user و هم weather را به عنوان یک آرگومان دریافت خواهد کرد.

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(',')),
      success(weather) {
        resolve({ user, weather: weather.query.results })
      },
      error: reject,
    })
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((data) => {
      // حال داده یک آبجکت، با یک ویژگی آب و هوا و یک ویژگی کاربر است

updateUI(data)
    })
    .catch(showError)
})

شما می‌توانید در این لینک با کد نهایی کار کنید.

در handler کلیک ما، شما می‌توانید قدرت درخشنده promiseها را در مقابل callbackها ببینید.

// Callbacks ?
getUser("tylermcginnis", (user) => {
  getWeather(user, (weather) => {
    updateUI({
      user,
      weather: weather.query.results
    })
  }, showError)
}, showError)

// Promises ✅
getUser("tylermcginnis")
  .then(getWeather)
  .then((data) => updateUI(data))
  .catch(showError);

دنبال کردن این منطق، یک نوع حس طبیعی دارد؛ زیرا ما به فکر کردن به همین روش عادت داریم، یعنی به ترتیب. getUser، سپس getWeather، و سپس رابط کاربری را با داده‌ها بروزرسانی کن.

حال واضح است که promiseها به شدت خوانایی کد ناهمگام ما را افزایش می‌دهند، اما آیا راهی وجود دارد که بتوانیم آن را حتی بهتر کنیم؟ فرض کنید که شما در کمیته TC39 هستید و تمام قدرت مورد نیاز برای اضافه کردن یک امکان جدید به زبان JavaScript را داشتید. چه قدم‌هایی را برای ارتقای این کد انجام خواهید داد؟

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((data) => updateUI(data))
    .catch(showError)
})

همانطور که درباره‌اش بحث کردیم، این کد به خوبی خوانده می‌شود. همانطور که مغزهای ما کار می‌کنند، این کد یک حالت به ترتیب دارد. یک مشکل که به آن بر خوردیم، این است که ما مجبور بودیم تمام داده‌ها، (users) از اولین درخواست ناهمگام تا آخرین .then را به هم رشته کنیم. این مسئله بزرگی نبود، اما ما را مجبور کرد تا تابع getWeather خود را تغییر دهیم تا users را منتقل کنیم. اگر کد ناهمگام خود را به همان روشی که کد همگام خود را نوشتیم، بنویسیم چه؟ اگر این کار را انجام دهیم، مشکل کاملا برطرف خواهد شد و همچنان به ترتیب خوانده خواهد شد.

$("#btn").on("click", () => {
  const user = getUser('tylermcginnis')
  const weather = getWeather(user)

updateUI({
    user,
    weather,
  })
})

خب، این کار خوب خواهد بود. کد ناهمگام ما دقیقا به مانند کد همگام ما است. هیچ قدم‌های اضافی‌ای وجود ندارد که متغیر ما به آن نیاز داشته باشد؛ زیرا ما از پیش با این طرز تفکر بسیار آشنا هستیم. متاسفانه، واضح است که این روش کار نخواهد کرد. همانطور که می‌دانید، اگر ما کد بالا را اجرا کنیم، هم user و هم weather فقط دو promise خواهند بود؛ زیرا آن چیزی است که getUser و getWeather بر می‌گردانند. اما به یاد داشته باشید، ما در TC39 هستیم. ما تمام قدرت مورد نیاز برای اضافه کردن یک امکان به زبانی که می‌خواهیم را داریم. کار کردن این کد به همین صورتی که هست، بسیار گیج کننده خواهد بود. ما مجبور خواهیم بود که به گونه‌ای به موتور JavaScript آموزش دهیم که تفاوت بین فراخوانی‌های تابع ناهمگام و معمولی را بداند. بیایید چند کلمه کلیدی به کد خود اضافه کنیم تا کار را برای موتور آسان‌تر کنیم.

در ابتدا، یک کلمه کلیدی اضافه می‌کنیم تا خود تابع را نگه دارد. این کار می‌تواند به موتور اطلاع دهد که داخل این تابع، ما قرار است برخی فراخوانی‌های تابع ناهمگام داشته باشیم. بیایید از async برای این مورد استفاده کنیم.

$("#btn").on("click", async () => {
  const user = getUser('tylermcginnis')
  const weather = getWeather(user)

updateUI({
    user,
    weather,
  })
}

خب، این کد منطقی به نظر می‌رسد. سپس یک کلمه کلیدی دیگر اضافه می‌کنیم تا موتور بداند که یک تابع دقیقا در چه زمانی فراخوانی می‌شود، و این که قرار است یک promise را برگرداند. بیایید از await استفاده کنیم. به این صورت که: «هی موتور، این تابع ناهمگام است و یک promise را بر می‌گرداند. به جای ادامه دادن به مانند معمول، ادامه بده و منتظر (await) مقدار نهایی promise باشد و قبل از ادامه دادن، آن را برگردان.» با داشتن هر دو کلمه async و await در کد، کد جدید ما چنین ظاهری خواهد داشت:

$("#btn").on("click", async () => {
  const user = await getUser('tylermcginnis')
  const weather = await getWeather(user.location)

updateUI({
    user,
    weather,
  })
})

ما یک راه منطقی برای داشتن کد ناهمگام خود اختراع کردیم، که همچنان می‌توانیم وانمود کنیم که این کد همگام است. حال قدم بعدی این است که یک شخص در EC39 را قانع کنیم که این یک ایده خوب است. خوشبختانه همانطور که تا به حال هم حدس زده‌اید، نیازی که نیست که کسی را قانع کنیم؛ زیرا این امکان از پیش بخشی از JavaScript است و Async / Await نام دارد.

در این لینک می‌توانید کد ما را مشاهده کنید که Async / Await را به آن اضافه کرده‌ایم.

توابع async یک promise را بر می‌گردانند

حال که منفعت Async / Await را دیده‌اید، بیایید برخی جزئیات مشابه را که دانستنشان مهم است، بررسی کنیم. در ابتدا، هر زمان که شما add را به یک تابع اضافه می‌کنید، آن تابع به طور ضمنی یک promise را بر خواهد گرداند.

async function getPromise(){}

const promise = getPromise()

حتی با این که getPromise علنا خالی است، همچنان یک promise را باز خواهد گرداند؛ زیرا یک تابع async است.

اگر تابع async یک مقدار را برگرداند، آن مقدار هم در یک Promise جمع‌بندی خواهد شد. این یعنی شما باید از .then برای دسترسی به آن استفاده کنید.

async function add (x, y) {
  return x + y
}

add(2,3).then((result) => {
  console.log(result) // 5
})

await بدون async بد است

اگر تلاش کنید که از کلمه کلیدی await در داخل یک تابع استفاده کنید که async نیست، با یک خطا مواجه خواهید شد.

$("#btn").on("click", () => {
  const user = await getUser('tylermcginnis') // SyntaxError: await is a reserved word
  const weather = await getWeather(user.location) // SyntaxError: await is a reserved word

updateUI({
    user,
    weather,
  })
})

من به این صورت درباره آن فکر می‌کنم. وقتی که شما async را به یک تابع اضافه می‌کنید، دو کار را انجام می‌دهد. به این صورت که خود تابع یک promise را بر می‌گرداند (یا چیزی که دریافت می‌کند را در یک promise جمع‌بندی می‌کند) و طوری عمل می‌کند که بتوانید از await داخل آن استفاده کنید.

مدیریت خطا

ممکن است متوجه شده باشید که ما کمی تقلب کردیم. ما در کد اصلی خود راهی برای گرفتن خطاها با استفاده از .catch داشتیم. وقتی که به Async / Await رفتیم، آن کد را حذف کردیم. در Async / Await، رایج‌ترین رویکرد این است که کد را در یک بلوک try / catch جمع‌بندی کنیم تا بتوانیم خطا را دریافت کنیم.

$("#btn").on("click", async () => {
  try {
    const user = await getUser('tylermcginnis')
    const weather = await getWeather(user.location)

updateUI({
      user,
      weather,
    })
  } catch (e) {
    showError(e)
  }
})

مقالات مرتبط:

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
عرفان کاکایی @er79ka
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو