تکامل جاوااسکریپت ناهمگام: از callbackها، تا promiseها و Async / Await
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 11 دقیقه

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

یکی از وبسایت‌های مورد علاقه من، وبسایت BerkshireHathaway.com است. این وبسایت ساده است و از سال ۱۹۹۷ که راه‌اندازی شد، در حال کار بوده است. حتی قابل توجه‌تر این که در طی ۲۰ سال اخیر، احتمالا این وبسایت هیچ باگی نداشته است.

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

به نظر می‌رسد اگر شما تمام داده‌ها را در مقابل خود داشته باشید، ساخت وبسایت‌ها بسیار ساده است. متاسفانه، امروزه اکثر وبسایت‌ها این داده‌ها را ندارند. برای جبران کردن آن، ما «الگوهایی» برای آوردن داده‌های خارجی برای برنامه‌مان اختراع کرده‌ایم.

به مانند اکثر چیزها، این الگوها هم تغییراتی در طی گذر زمان داشته‌اند. در این پست ما نکات مثبت و منفی سه الگوی رایج، یعنی Callbackها، Promiseها و Async / Await را بررسی خواهیم کرد و درباره اهمیت و پیشرفتشان از یک زمینه تاریخی صحبت خواهیم کرد.

بیایید با الگوی آوردن داده بزرگ‌تر، یعنی Callbackها شروع کنیم.

Callbackها

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

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

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

add(2,3)

اهمیتی ندارد که من دکمه را فشار دهم، شما دکمه را فشار دهید یا کس دیگری دکمه را فشار دهد. هر زمان که دکمه فشرده شده است، چه دوست داشته باشید یا نه این ماشین قرار است اجرا شود.

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

const me = add
const you = add
const someoneElse = add

me(2,3) // 5 - دکمه را فشار بده، ماشین را اجرا کن.
you(2,3) // 5 - دکمه را فشار بده، ماشین را اجرا کن.
someoneElse(2,3) // 5 - دکمه را فشار بده، ماشین را اجرا کن.

در کد بالا ما تابع add را به سه متغیر متفاوت me، you و someoneElse اختصاص می‌دهیم. مهم است دقت کنید که add اصلی و هر کدام از متغیرهایی که ما ساختیم، در حال اشاره به نقطه مشابهی بر روی رم هستند. این موارد دقیقا چیز مشابهی تحت نام‌های متفاوت هستند. پس وقتی ما me، you یا someoneElse را فراخوانی می‌کنیم، به مانند این است که add را فراخوانی کنیم.

حال اگر ماشین add خود را بگیریم و آن را به یک ماشین دیگر منتقل کنیم چه؟ به یاد داشته باشید که اهمیتی ندارد چه کسی دکمه را فشار دهد؛ اگر این دکمه فشرده شود، ماشین اجرا خواهد شد.

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

function addFive (x, addReference) {
  return addReference(x, 5) // 15 - دکمه را فشار بده، ماشین را اجرا کن.
}

addFive(10, add) // 15

ممکن است مغز شما سر این مورد کمی گیج شده باشد، اما در اینجا هیچ اتفاق جدیدی نمی‌افتد. به جای فشردن دکمه add، ما آن را به عنوان یک آرگومان به addFive منتقل می‌کنیم، آن را به addReference مجددا نامگذاری می‌کنیم و سپس دکمه را می‌فشاریم یا آن را فراخوانی می‌کنیم.

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

از‌ آنجایی که واژگان مهم هستند، در اینجا همین کد را می‌بینید که در آن متغیرها به گونه‌ای مجددا نامگذاری شده‌اند تا با مفهومی که نمایش می‌دهند تطابق داشته باشند.

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

function higherOrderFunction (x, callback) {
  return callback(x, 5)
}

higherOrderFunction(10, add)

این الگو باید آشنا به نظر بیاید، زیرا در همه جا وجود دارد. اگر از هر کدام از متدهای آرایه JavaScript استفاده کرده باشید، پس از یک callback هم استفاده کرده‌اید. اگر از lodash استفاده کرده باشد، از یک callback هم استفاده کرده‌اید. اگر از jQuery استفاده کرده باشید، از یک callback هم استفاده کرده‌اید.

[1,2,3].map((i) => i + 5)

_.filter([1,2,3,4], (n) => n % 2 === 0 );

$('#btn').on('click', () =>
  console.log('Callbacks are everywhere')
)

به طور عمومی، دو موقعیت استفاده معروف برای callbackها وجود دارد. اولین موقعیت و چیزی که در مثال‌های .map و _.filter می‌بینیم، یک چکیده عالی برای تبدیل یک مقدار به دیگری است. ما می‌گوییم: «هی، در اینجا یک آرایه و یک تابع وجود دارد. ادامه بده و مقدار را به پایه تابعی که به تو دادم بیاور.» موقعیت دوم و چیزی که در مثال jQuery می‌بینیم، اجرای یک تابع را تا زمانی خاص به تاخیر می‌اندازد. «هی، در اینجا یک تابع وجود دارد. ادامه بده و هر زمان که بر روی عنصر دارای آیدی btn کلیک شده است، آن را فراخوانی کن.» در موقعیت استفاده دوم است که ما می‌خواهیم بر رویش تمرکز کنیم: «به تاخیر انداختن اجرای یک تابع، تا زمانی خاص.»

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

نیازی به خیال‌پردازی زیادی نیست تا ببینیم که چگونه می‌توانیم آن جمله را به کار بگیریم تا با آوردن داده کار کند. به جای تاخیر اجرای یک تابع تا زمانی خاص، ما می‌توانیم اجرای آن را تا زمانی که داده‌های مورد نیاز را داریم، به تاخیر بیندازیم. در اینجا مثالی را می‌بینید که احتمالا معروف‌ترین مثال است، یعنی متد getJson در جی‌کوئری:

// updateUI and showError are irrelevant.
// وانمود کن کاری که به نظر می‌آید را انجام می‌دهند

const id = 'tylermcginnis'

$.getJSON({
  url: `https://api.github.com/users/${id}`,
  success: updateUI,
  error: showError,
})

ما نمی‌توانیم رابط کاربری برنامه‌مان را تا زمانی که داده‌های کاربر را داریم، بروزرسانی کنیم. پس چه کاری انجام می‌دهیم؟ ما می‌گوییم: «هی، یک آبجکت در اینجا وجود دارد. اگر درخواست با موفقیت انجام شود، ادامه بده و با انتقال داده‌های کاربر به success، آن را فراخوانی کن. اگر با موفقیت انجام نشد، ادامه بده و با انتقال داده‌های کاربر به error، آن را فراخوانی کن. نیازی نیست که نگران باشید هر متد چه کاری انجام می‌دهد، فقط مطمئن شوید که وقتی باید، آن‌ها را فراخوانی می‌کنید.» این یک نمایش عالی برای استفاده از یک callback برای درخواست‌های async است.

تا به اینجا یاد گرفته‌ایم که callbackها چه هستند و چگونه می‌توانند هم در کد همگام و هم در کد ناهمگام، سودمند باشند. چیزی که هنوز درباره‌اش صحبت نکرده‌ایم، نیمه تاریک callbackها است. نگاهی به کد زیر داشته باشید. آیا می‌توانید بگویید که چه اتفاقی می‌افتد؟

// updateUI, showError, and getLocationURL are irrelevant.
// وانمود کن کاری که به نظر می‌آید را انجام می‌دهند

const id = 'tylermcginnis'

$("#btn").on("click", () => {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: (user) => {
      $.getJSON({
        url: getLocationURL(user.location.split(',')),
        success (weather) {
          updateUI({
            user,
            weather: weather.query.results
          })
        },
        error: showError,
      })
    },
    error: showError
  })
})

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

دقت کنید که ما چند لایه دیگر از callbackها اضافه کرده‌ایم. در ابتدا ما می‌گوییم تا زمانی که بر روی عنصر دارای آیدی btn کلیک شده است، درخواست AJAX اولیه را اجرا نکن. وقتی که بر روی دکمه کلیک شده است، اول درخواست را اجرا می‌کنیم. اگر این درخواست با موفقیت اجرا شود، یک درخواست ثانویه را ارسال می‌کنیم. اگر آن درخواست با موفقیت انجام شود، ما متد updateUI را با انتقال داده‌هایی که از هر دو درخواست گرفتیم، فراخوانی می‌کنیم. بدون توجه به این که آیا در نگاه اول کد را درک می‌کنید یا نه، واضح است که خواندن قبل از کدنویسی، بسیار سخت‌تر است. این ما را به موضوع «Callback Hell» می‌آورد.

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

به مانند اکثر راه حل‌ها برای مشکلات نرم‌افزاری، یک رویکرد تجویز شده رایج برای ساد‌ه‌تر کردن هضم Callback Hell، این است که کد خود را ماژولار کنید.

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)
})

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

خب، نام‌های تابع به ما کمک می‌کنند تا درک کنیم که چه اتفاقی می‌افتد، اما آیا واضح است که این روش بهتر است؟ زیاد نه. ما یک بانداژ بر روی مشکل خوانایی Callback Hell قرار داده‌ایم. این مشکل که ما به طور طبیعی به ترتیب فکر می‌کنیم، هنوز وجود دارد و حتی با توابع اضافی، Callback Hellهای تو در تو ما را از روش تفکر ترتیبی خارج می‌کنند.

مشکل بعدی Callbackها، در معکوس شدن کنترل است. وقتی که شما یک Callback را می‌نویسید، فرض می‌کنید که برنامه‌ای که callback را به آن می‌دهید، مسئول فراخوانی است و وقتی که باید (و فقط همان موقع) آن را فراخوانی می‌کند. شما لزوما کنترل برنامه خود را به یک برنامه دیگر معکوس می‌کنید. وقتی که با کتابخانه‌هایی مانند jQuery، lodash یا حتی Vanilla JavaScript سر و کله می‌زنید، امن‌تر این است که فرض کنید تابع callback در زمان مناسب و با آرگومان‌های مناسب فراخوانی خواهد شد. گرچه برای بسیاری کتابخانه‌ها، توابع callback رابطی برای نحوه تعامل شما با آنان هستند. این مسئله که یک کتابخانه چه از روی عمد یا چه ب طور اتفاقی می‌تواند نحوه تعامل با callback شما را بشکند، کاملا پذیرفتنی است.

function criticalFunction () {
  // حیاتی است که این تابع با آرگومان‌های صحیح فراخوانی شود
}

thirdPartyLib(criticalFunction)

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

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

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

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
4 از 1 رای

/@er79ka

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

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

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