درک درخواست‌های Async در Node.js

Async Requests in Node.js

28 بهمن 1399
درک درخواست های Async در Node.js

اگر با Node.js کار کرده باشید حتما اسم درخواست‌های Async در Node.js را شنیده‌اید. زمانی که با Node کار می‌کنیم و می‌خواهیم فقط یک وب‌سایت ساده را بسازیم، معمولا با چنین درخواست‌هایی روبرو نمی‌شویم اما زمانی که بخواهید با سیستم دیگری (مثلا یک API) صحبت کنید، اوضاع متفاوت خواهد بود.

پیش‌نیاز: این مقاله برای خوانندگان مبتدی نوشته‌نشده است بنابراین برای مطالعه و درک کامل آن، باید دانش نسبی از جاوا اسکریپت داشته باشید.

ماهیت درخواست‌های async و callback ها

ابتدا با ماهیت این نوع درخواست‌ها شروع می‌کنیم. async (مخفف asynchronous) به معنای «ناهمگام» است (البته در وب فارسی بعضا با نام نامتقارن نیز به آن اشاره می‌شود). زمانی که یک وب‌سایت ساده‌ی HTML ای داشته باشیم، تعامل سرور و کلاینت (مرورگر) به شکل زیر خواهد بود:

  • کلاینت درخواستی را ارسال می‌کند.
  • سرور درخواست را پردازش می‌کند.
  • سرور پاسخ مناسب را ارسال می‌کند.
  • کلاینت پاسخ را دریافت می‌کند.

در وب‌سایت‌های ساده‌ی HTML، پاسخ سرور همیشه HTML است و داده‌ی خاصی نخواهیم داشت. از طرف دیگر اگر برنامه‌ی پیشرفته‌تری داشته باشیم (مثل یک REST API) داده‌هایمان دیگر HTML نیستند بلکه تقریباً در ۱۰۰% موارد از JSON استفاده می‌کنیم. در این برنامه‌ها هنوز هم ساختار ارسال درخواست و دریافت پاسخ با مراحلی که بالاتر ذکر کردم یکی است اما تصور کنید که در هر نوع از این دو ساختار بخواهیم با یک سرور دیگر صحبت کنیم. یعنی چه؟ بگذارید یک مثال ساده بزنم. فرض کنید کاربر ما، شهر خود را برای ما ارسال می‌کند و از ما اطلاعات هواشناسی‌اش را می‌خواهد. طبیعتا ما تجهیزات و دانش کافی را در این زمینه نداریم بنابراین باید به یک سرور دیگر (API) متصل شویم که چنین داده‌هایی را دارد، این داده‌ها را دریافت کنیم و سپس به کاربر ارسال کنیم. طبیعتا چنین فرآیندی زمان خواهد برد و ازآنجایی‌که Node.js یک فنّاوری async یا ناهمگام است، منتظر دریافت داده‌ها نمی‌شود. یعنی چه؟ به شبه کد زیر توجه کنید:

log "before req"

get city from user

send city to api

get data from api

send data to user

log "after req"

احتمالا انتظار دارید که شبه کد بالا خط به خط اجرا شود تا ابتدا متن before req و سپس متن after req را دریافت کنیم اما این‌طور نیست! در این مثال ابتدا متن before req و سپس متن after req اجرا می‌شوند و درنهایت داده‌ها به کاربر ارسال می‌شوند!

برای درک بهتر به سراغ یک مثال ساده می‌رویم. یک فایل به نام roxo.txt را ایجاد کرده و متن دلخواهی را در آن بنویسید. مثلا:

You can learn development at roxo.ir/plus for free!

حالا در کنار همین فایل یک فایل دیگر به نام fileRead.js ایجاد می‌کنیم:

const fs = require('fs')







console.log("1", 'Before Reading The File')




const file = fs.readFileSync('./roxo.txt', 'utf-8')




console.log("2", file)




console.log("3", 'After Reading The File')

همان‌طور که می‌بینید من در این فایل ماژول file system (همان fs) را وارد کرده‌ام و سپس از متد readFileSync برای خواندن محتویات فایل استفاده کرده‌ام. sync در انتهای نام readFileSync بدین مسئله اشاره می‌کند که این متد synchronous یا «همگام» است (برخلاف asynchronous یا «ناهمگام»). یعنی چه؟ یعنی تا زمانی که این کد به‌صورت کامل اجرا نشود، خطوط بعدی برنامه اجرا نخواهند شد و برنامه منتظر می‌ماند. با اجرای کد بالا نتیجه‌ی زیر را دریافت می‌کنیم:

1 Before Reading The File

2 You can learn development at roxo.ir/plus for free!




3 After Reading The File

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

const fs = require('fs')







console.log("1", 'Before Reading The File')




const file = fs.readFile('./roxo.txt')




console.log("2", file)




console.log("3", 'After Reading The File')

تفاوت readFile و readFileSync در این است که readFile ناهمگام یا async است! با اجرای این کد نتیجه‌ی زیر را می‌گیریم:

1 Before Reading The File

node:fs:169

  throw new ERR_INVALID_CALLBACK(cb);

  ^




TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined

    at new NodeError (node:internal/errors:278:15)

    at maybeCallback (node:fs:169:9)

    at Object.readFile (node:fs:320:14)

    at Object.<anonymous> (/mnt/Development/Roxo Academy/Node.js/02-Understanding Async/code/fileRead.js:7:17)

    at Module._compile (node:internal/modules/cjs/loader:1102:14)

    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1131:10)

    at Module.load (node:internal/modules/cjs/loader:967:32)

    at Function.Module._load (node:internal/modules/cjs/loader:807:14)

    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)

    at node:internal/main/run_main_module:17:47 {

  code: 'ERR_INVALID_CALLBACK'

}

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

const fs = require('fs')







console.log("1", 'Before Reading The File')




const file = fs.readFile('./roxo.txt', () => {console.log("2", 'function finished')})




console.log("3", file)




console.log("4", 'After Reading The File')

همان‌طور که می‌بینید من یک تابع را به‌عنوان آرگومان دوم readFile پاس داده‌ام که تنها کارش log کردن عبارت function finished (به معنی «کار تابع تمام‌شده است») است. حالا اگر کد بالا را اجرا کنیم چه نتیجه‌ای می‌گیریم؟

1 Before Reading The File

3 undefined

4 After Reading The File

2 function finished

احتمالا تعجب کرده باشید. این کد خط به خط اجرا نشده است! به‌طور مثال بااینکه دستور log شماره‌ی ۲ در کد قبل از دستورات ۳ و ۴ بوده است اما در این کد آخرین دستور اجرا شده است! آیا می‌دانید چرا چنین نتیجه‌ای را گرفته‌ایم؟ مسئله اینجاست که دستوراتی مانند log کردن یک رشته‌ی ساده در کسری از ثانیه (در حد میلی‌ثانیه) انجام می‌شوند درصورتی‌که دستوراتی مانند خواندن فایل از دیسک استفاده کرده و محتوای فایل را درون مموری خالی می‌کنند بنابراین بیشتر طول می‌کشند. دلیل undefined بودن دستور log دوم نیز همین است؛ قبل از اینکه محتوای فایل خوانده شود می‌خواهیم آن را در دستور log دوم چاپ کنیم و طبیعتا ازآنجایی‌که هنوز هیچ مقداری از فایل خوانده‌نشده است نتیجه‌ای برای چاپ شدن وجود ندارد. راه‌حل چیست؟ ما باید داده‌ی خود را درون این callback چاپ کنیم چراکه callback فقط زمانی اجرا می‌شود که عملیات fileRead تمام‌شده باشد:

const fs = require('fs')







console.log("1", 'Before Reading The File')




const file = fs.readFile('./roxo.txt', 'utf-8' ,(err, data) => {

                console.log("ERROR: ", err)

                console.log("DATA: ", data)

})




console.log("3", file)




console.log("4", 'After Reading The File')

نکته‌ی مهم در کد بالا این است که من encoding را روی utf-8 گذاشته‌ام تا کد ما به‌صورت buffer خوانده نشود. در مرحله‌ی بعدی callback را پاس داده‌ایم. باید بدانید که این callback دو آرگومان می‌گیرد: آرگومان اول خطاهای ممکن در هنگام خواندن فایل و آرگومان دوم داده‌های درون فایل خواهند بود. من هر دو را در قالب یک دستور log چاپ کرده‌ام. با اجرای کد بالا نتیجه‌ی بالا را می‌گیریم:

1 Before Reading The File

3 undefined

4 After Reading The File

ERROR:  null

DATA:  You can learn development at roxo.ir/plus for free!

همان‌طور که می‌بینید عملیات خواندن فایل بدون خطا بوده است بنابراین Error برابر null است اما DATA همان متنی است که ما در فایل خود داشته‌ایم (من متن فایل را دوباره به همین یک خط تغییر داده‌ام). درصورتی‌که نوع encoding را مشخص نکنیم، داده‌های برگردانده شده به‌صورت buffer خواهند بود. به مثال زیر توجه کنید:

1 Before Reading The File

3 undefined

4 After Reading The File

ERROR:  null

DATA:  <Buffer 59 6f 75 20 63 61 6e 20 6c 65 61 72 6e 20 64 65 76 65 6c 6f 70 6d 65 6e 74 20 61 74 20 72 6f 78 6f 2e 69 72 2f 70 6c 75 73 20 66 6f 72 20 66 72 65 65 ... 2 more bytes>

داده‌های buffer ای که در قسمت DATA مشاهده می‌کنید همان داده‌های فایل ما هستند.

مثال‌های دیگری از callback ها، توابع setTimeout و addEventListener هستند:

window.addEventListener('load', () => {

  //window loaded

  //do what you want

})







setTimeout(() => {

  // runs after 2 seconds

}, 2000)

مشکل callback ها

سیستم‌های کامپیوتری ماهیتا async یا ناهمگام هستند بنابراین باید راهی را برای مدیریت این رفتار پیدا کنیم. راه اولی که در بخش قبل توضیح دادم یا همان callback ها یکی از روش‌های مدیریت رفتار غیرهمگام کامپیوترها هستند اما احتمالا شما هم می‌توانید مشکل callback ها را ببینید. به کد زیر توجه کنید:

window.addEventListener('load', () => {

  document.getElementById('button').addEventListener('click', () => {

    setTimeout(() => {

      items.forEach(item => {

        //your code here

      })

    }, 2000)

  })

})

اینجاست که با مشکل callback ها آشنا می‌شویم؛ callback ها توابعی در هم هستند بنابراین اگر با کدهای ناهمگامی کارکنیم که باعث تشکیل callback های تودرتو می‌شوند، ویرایش و خواندن آن‌ها در آینده تقریبا غیرممکن می‌شود! در کد بالا ۴ سطح تودرتو را داریم و شاید تصور کنید که هیچ‌گاه چنین موقعیتی پیش نمی‌آید اما اگر واقعا با API ها و کدهای پیشرفته‌تر کار کرده باشید حتما به کدهایی بدتر از این کد نیز برخورد کرده اید. کد زیر یک مثال ساده از پدیده‌ای به نام callback hell (جهنم callback ای) است:

fs.readdir(source, function (err, files) {

  if (err) {

    console.log('Error finding files: ' + err)

  } else {

    files.forEach(function (filename, fileIndex) {

      console.log(filename)

      gm(source + filename).size(function (err, values) {

        if (err) {

          console.log('Error identifying file size: ' + err)

        } else {

          console.log(filename + ' : ' + values)

          aspect = (values.width / values.height)

          widths.forEach(function (width, widthIndex) {

            height = Math.round(width / aspect)

            console.log('resizing ' + filename + 'to ' + height + 'x' + height)

            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {

              if (err) console.log('Error writing file: ' + err)

            })

          }.bind(this))

        }

      })

    })

  }

})

کد بالا بین فایل‌های مختلف یک پوشه گردش کرده و آن‌ها را ویرایش می‌کند. همه‌ی ما می‌دانیم که برای خواندن چنین کدی باید زمان زیادی وقت گذاشته شود و عملا وقت خودمان و دیگران را تلف‌کرده‌ایم! برای درک بهتر می‌توانید به مقاله‌ی Pyramid of Doom در ویکی پدیا نیز نگاهی بیندازید.

راه‌حل ممکن: از کد ناهمگام استفاده نکنیم!

بااین‌همه سوالی برای همه‌ی ما پیش می‌آید. درصورتی‌که کدهای ناهمگام تا این حجم دردسرساز هستند چرا از آن‌ها استفاده می‌کنیم؟ آیا برای حل مشکل callback ها بهتر نیست کدهای ناهمگام را کنار بگذاریم؟ برای پاسخ به این سوال باید به مثال قبلی خودمان برگردیم:

const fs = require('fs')







console.log("1", 'Before Reading The File')




const file = fs.readFile('./roxo.txt', 'utf-8' ,(err, data) => {

                console.log("ERROR: ", err)

                console.log("DATA: ", data)

})




console.log("3", file)




console.log("4", 'After Reading The File')

فرض کنید که فایل roxo.txt حجم بسیار زیادی داشته باشد. طبیعتا خواندن این فایل زمان قابل‌توجهی را به خود اختصاص خواهد داد. اگر بخواهیم این عملیات را به‌صورت همگام اجرا کنیم، روند اجرای برنامه در تابع readFile قفل می‌شود. چرا؟ به دلیل اینکه برنامه سعی می‌کند ابتدا فایل را بخواند و سپس به سراغ خطوط بعدی کد برود. این مسئله یعنی اجرای برنامه‌ی ما در عمل تا چند ثانیه متوقف می‌شود. حالا می‌توانید این مسئله را در سطح سرورها تصور کنید؛ اگر هر کاربر با یک درخواست ساده روند اجرای برنامه را متوقف کند، سرعت سرور تا حد زیادی کاهش پیدا خواهد کرد بنابراین کدهای ناهمگام یکی از مهم‌ترین نقاط قوت جاوا اسکریپت هستند و نمی‌توانیم آن‌ها را نادیده بگیریم.

راه‌حل عملی: استفاده از promise ها

promise ها یکی از ابداعات جدید در جاوا اسکریپت بودند که در چند سال گذشته معرفی‌شده‌اند. promise به معنای «قول» یا «عهد» است و همان‌طور که از معنی آن مشخص است، کارش برگرداندن یک مقدار به شماست البته زمانی که در دسترس باشد! به زبان ساده‌تر به شما قول می‌دهد که در هنگام اتمام یک عملیات داده‌ای را برگرداند یا کاری را برایتان انجام بدهد. promise ها از دو قسمت then و catch ساخته می‌شوند که به ترتیب برای دریافت نتیجه و دریافت خطا استفاده می‌شوند. برای  درک بهتر این موضوع بهتر است مثال خواندن فایل roxo.txt را با promise ها بنویسیم:

const fs = require('fs').promises







console.log("1", 'Before Reading The File')




const file = fs.readFile('./roxo.txt', 'utf-8')

   .then(data => {console.log("Inside THEN", data)})

   .catch(err => {console.log("Inside CATCH", err)})




console.log("4", 'After Reading The File')

همان‌طور که می‌بینید برای کار با promise ها باید دقیقا قسمت promises را از ماژول fs وارد کنیم در غیر این صورت نمی‌توانیم از promise ها استفاده کنیم. در قدم اول مثل همیشه تابع readFile را صدا زده‌ام اما این بار متدی به نام then را با علامت نقطه به آن متصل کرده‌ام و سپس متدی به نام catch را نیز به then متصل کرده‌ایم. کدهای درون بلوک then زمانی اجرا خواهند شد که عملیات readFile به‌صورت موفقیت‌آمیز تمام‌شده باشد. از طرف دیگر کدهای درون بلوک catch زمانی اجرا می‌شوند که خطایی در انجام این عملیات رخ‌داده باشد. در ضمن همیشه باید یک تابع را به then و catch پاس بدهید و عملیات موردنظر خود را درون این توابع انجام بدهید. با اجرای این کد نتیجه‌ی زیر را می‌گیریم:

1 Before Reading The File

4 After Reading The File

Inside THEN You can learn development at roxo.ir/plus for free!

همان‌طور که می‌بینید روند اجرا کد مانند callback ها است بنابراین می‌دانیم که promise ها قابلیت خاصی نیستند بلکه روش بهتری برای نوشتن کد هستند تا کدهایمان خواناتر بوده و وارد callback hell نشویم. اگر به نتیجه‌ی بالا دقت کنید متوجه خواهید شد که دستور log درون catch اجرا نشده است. چرا؟ به دلیل اینکه در خواندن این فایل خطایی نداشته‌ایم بنابراین کدهای درون بلوک catch اجرا نمی‌شوند. برای ایجاد خطا می‌توانیم نام فایل را به‌اشتباه بنویسیم:

const fs = require('fs').promises







console.log("1", 'Before Reading The File')




const file = fs.readFile('./roxo.tx', 'utf-8')

   .then(data => {console.log("Inside THEN", data)})

   .catch(err => {console.log("Inside CATCH", err)})




console.log("4", 'After Reading The File')

پسوند txt در کد بالا به اشتباه tx نوشته شده است. حالا اگر کد را اجرا کنیم نتیجه‌ی زیر را دریافت می‌کنیم:

1 Before Reading The File

4 After Reading The File

Inside CATCH [Error: ENOENT: no such file or directory, open './roxo.tx'] {

  errno: -2,

  code: 'ENOENT',

  syscall: 'open',

  path: './roxo.tx'

}

این بار کدهای درون then اجرانشده‌اند اما کدهای درون catch اجراشده‌اند. خطایی که ما دریافت کرده‌ایم می‌گوید no such file or directory که در زبان فارسی یعنی «چنین فایل یا پوشه‌ای وجود ندارد». همان‌طور که می‌بینید این روش مدیریت کد بسیار ساده است. در این روش readFile یک promise را برمی‌گرداند و به همین دلیل است که می‌توانیم متدهای then و catch را روی آن صدا بزنیم اما سوالی پیش می‌آید: چطور می‌توانیم خودمان promise بسازیم؟ به کد زیر توجه کنید:

const fs = require('fs')




const getFile = (fileName) => {

  return new Promise((resolve, reject) => {

    fs.readFile(fileName, 'utf-8', (err, data) => {

      if (err) {

        reject(err)

        return

      }

      resolve(data)

    })

  })

}




getFile('./roxo.txt')

.then(data => console.log(data))

.catch(err => console.error(err))

من عامدانه callback ها و promise ها را در کد بالا ترکیب کرده‌ام. در کد بالا متدی به نام getFile را داریم که نام فایلی را گرفته و سپس آن را می‌خواند اما در callback خود یک شرط if را نوشته‌ایم. در ابتدا باید توجه کنید که برای ساخت promise باید از new Promise استفاده کنیم و طبیعتا باید آن را return کنیم تا در قسمت then و catch قابل‌دسترس باشند. هر promise در هنگام ساخته‌شدن، دو آرگومان می‌گیرد که هر دو تابع هستند: reject (رد درخواست یا همان بروز خطا) و resolve (قبول درخواست). من در قسمت callback و پس از خواندن فایل با دو حالت روبرو خواهم شد:

  • خواندن فایل موفقیت‌آمیز بوده است.
  • خواندن فایل با خطا مواجه شده است.

ما می‌توانیم وجود خطا را با یک شرط if ساده بررسی کنیم بنابراین اگر خطایی در کار باشد reject را صدا زده و آن خطا را به‌صورت یک آرگومان پاس می‌دهیم و در غیر این صورت resolve را صدا زده و داده‌ها (محتویات فایل) را به آن می‌دهیم. در نظر داشته باشید که هر داده‌ای را به reject بدهید در قسمت catch و هر داده‌ای را به resolve بدهید در قسمت then دریافت خواهید کرد. با اجرای کد بالا نتیجه‌ی زیر را دریافت می‌کنیم:

You can learn development at roxo.ir/plus for free!

در صورتی که نام فایل را به نام اشتباهی تغییر بدهید نیز نتیجه‌ی زیر را دریافت خواهیم کرد:

[Error: ENOENT: no such file or directory, open './roxso.txt'] {

  errno: -2,

  code: 'ENOENT',

  syscall: 'open',

  path: './roxso.txt'

}

همچنین در نظر داشته باشید که می توانید درون یک promise یک promise دیگر را ایجاد کنید و یا اینکه درون قسمتی از کدها دو promise را داشته باشید. در این صورت می‌توانیم promise ها را به هم زنجیر کنیم:

const status = response => {

  if (response.status >= 200 && response.status < 300) {

    return Promise.resolve(response)

  }

  return Promise.reject(new Error(response.statusText))

}




const json = response => response.json()




fetch('/todos.json')

  .then(status)   

  .then(json)     

  .then(data => {

    console.log('Request succeeded with JSON response', data)

  })

  .catch(error => {

    console.log('Request failed', error)

  })

این کد بخشی از کدهای یک سرور Node.js است بنابراین به‌صورت مستقل اجرا نمی‌شود اما برای درک مفهوم اصلی زنجیر کردن promise ها مفید است. ما در کد بالا دو متد status و json را تعریف کرده‌ایم. متد status یک آرگومان به نام response می‌گیرد و درصورتی‌که خصوصیت status آن برابر ۲۰۰ یا بیشتر اما زیر ۳۰۰ باشد، response را در قالب یک promise برمی‌گرداند یا resolve می‌کند. متد json نیز داده‌ای را گرفته و مقدار آن را به‌صورت json برمی‌گرداند. درنهایت تابعی به نام fetch را صدا می‌زنیم که به سراغ آدرس todos.json می‌رود و داده‌ها را از آن می‌خواند. فرض می‌کنیم که با دریافت داده‌ها یک promise برگردانده می‌شود بنابراین then را به آن زنجیر می‌کنیم، سپس متد status اجرا خواهد شد (then این کار را انجام می‌دهد) که خودش یک promise را برمی‌گرداند بنابراین یک then دیگر را نیز زنجیر کرده‌ایم که متد json را اجرا می‌کند. در انتهای یک catch داریم که تمام خطاها از تمام then های قبلی را دریافت خواهد کرد. همان‌طور که مشاهده می‌کنید قدرت اصلی promise ها در این است که برخلاف callback ها تودرتو نیستند بنابراین از خوانا بودن کد کم نمی‌شود.

علاوه بر این promise ها دو قابلیت جالب دیگر را نیز اضافه کرده‌اند: promise.all و promise.race. قابلیت promise.all زمانی استفاده می‌شود که مانند مثال بالا چندین promise داشته باشیم و بخواهیم فقط زمانی که تمام promise ها کامل شدند، عملیاتی را انجام بدهیم. مثال:

const f1 = fetch('/something.json')

const f2 = fetch('/something2.json')




Promise.all([f1, f2])

  .then(res => {

    console.log('Array of results', res)

  })

  .catch(err => {

    console.error(err)

  })

در این حالت قسمت then و catch فقط زمانی اجرا می شود که هر دو تابع f1 و f2 اجرا شده باشند و promise خود را برگردانند. از طرف دیگر قابلیت promise.race به ما اجازه می‌دهد که فقط اولین promise کامل شده را مدیریت کنیم:

const first = new Promise((resolve, reject) => {

  setTimeout(resolve, 500, 'first')

})

const second = new Promise((resolve, reject) => {

  setTimeout(resolve, 100, 'second')

})




Promise.race([first, second]).then(result => {

  console.log(result) // second

})

در این کد promise دوم مدیریت خواهد شد و وارد then می‌شود چراکه زودتر از promise اول اجرا خواهد شد. کلمه‌ی race به معنی «مسابقه» است بنابراین احتمالا شما نیز متوجه این موضوع می‌شوید.

راه‌حل جدید: استفاده از async & await

promise ها در نسخه‌ی ES2015 معرفی شدند و بعدازآن در نسخه‌ی ES2017 قابلیت جدیدی به نام async await معرفی شد. البته async await واقعا قابلیت جدیدی نیست بلکه روشی جدید برای مدیریت promise ها است. بااینکه promise ها مشکل callback hell را حل کردند اما خودشان با اضافه کردن دستورات مختلف then و catch باعث پیچیدگی کد شدند، به همین دلیل روش جدیدی برای نوشتن آن‌ها معرفی شد. برای درک بهتر به کد زیر توجه کنید:

const doSomethingAsync = () => {

  return new Promise(resolve => {

    setTimeout(() => resolve('I did something'), 3000)

  })

}




const doSomething = async () => {

  console.log(await doSomethingAsync())

}




console.log('Before')

doSomething()

console.log('After')

ما در ابتدا متدی به نام  doSomethingAsync را داریم که با تابع setTimeout دقیقا ۳ ثانیه تاخیر ایجاد کرده و سپس عبارت I did something را در قالب یک promise برمی‌گرداند. در مرحله‌ی بعدی تابعی به نام doSomething را ایجاد کرده‌ایم که با کلیدواژه‌ی async تعریف‌شده است و درون آن از دستور await (به معنی «صبر کن») استفاده کرده‌ایم. دو نکته لازم به ذکر است:

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

 با اجرای کد بالا نتیجه‌ی زیر را دریافت می‌کنیم:

Before

After

I did something

برای ثابت کردن این موضوع که async تابع را مجبور به بازگردانی promise می‌کند می‌توانیم کد زیر را اجرا کنیم:

const aFunction = async () => {

  return 'test'

}




aFunction().then(alert) // برگردانده می شود 'test' عبارت

طبیعتا این استفاده از async/await بسیار تمیزتر و خوانا تر است. به مثال زیر توجه کنید:

const getFirstUserData = () => {

  return fetch('/users.json') // get users list

    .then(response => response.json()) // parse JSON

    .then(users => users[0]) // pick first user

    .then(user => fetch(`/users/${user.name}`)) // get user data

    .then(userResponse => userResponse.json()) // parse JSON

}




getFirstUserData()

در این کد از promise ها استفاده کرده‌ایم. حالا همین کد را با async/await می‌نویسیم:

const getFirstUserData = async () => {

  const response = await fetch('/users.json') // get users list

  const users = await response.json() // parse JSON

  const user = users[0] // pick first user

  const userResponse = await fetch(`/users/${user.name}`) // get user data

  const userData = await userResponse.json() // parse JSON

  return userData

}




getFirstUserData()

علاوه بر خواناتر بودن این کد، debug کردن آن نیز راحت‌تر است چرا که از نظر کامپایلر شبیه به کدهای همگام اجرا می‌شود (ترتیب اجرا خط‌به‌خط خواهد بود).

نویسنده شوید
دیدگاه‌های شما (1 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

حسین
24 بهمن 1400
پس پقتی از async await استفاده میکنیم میتونیم توابع ناهمگام رو بدون کالبک ها اجرا کنیم درسته ؟

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مش رجب
10 دی 1401
بله!

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.