Promiseها در جاوااسکریپت – بخش دوم

در بخش اول این مقاله گفتیم جاوااسکریپت از جمله زبان‌هایی است که اتفاقات غیر همزمان (asynchronous) در آن معمول و متداول است. گفتیم بعضی اوقات لازم است تا از جاوااسکریپت قول بگیریم که بعد از مشخص شدن تکلیف اجرای یک دستور غیر همزمان کاری را انجام دهد و برای این منظور Promiseها در جاوااسکریپت را معرفی کردیم. فرق آن را با Eventها با مثال مشخص کردیم؛ واژگان استفاده شده در رابطه با مفهوم Promise را تعریف کردیم. همچنین کتابخانه‌های معروفی که قبل از ورود این مفهوم به جاوااسکریپت استفاده می‌شد را معرفی کردیم. در این مقاله مبحث پیشین را ادامه می‌دهیم. سعی می‌کنیم نحوه‌ی تعامل با Promiseها را یک به یک مرور کرده و با مثال ابعاد مختلف این مفهوم را واکاوری کنیم.

چطور یک Promise تعریف کنیم؟

برای تعریف Promise از ساختار زیر استفاده می‌کنیم:

var promise = new Promise(function(resolve, reject) {
  // ye kari bokon, masalan ye kare async

  if (/* hame chi khob pish raft */) {
    resolve("kar anjam shod!");
  }
  else {
    reject(Error("kar dorost anjam nashod!"));
  }
});

ورودی Promise یک تابع callback است که دو متغیر به نام‌های resolve و reject دارد. می‌توانیم وظیفه‌ای را داخل این تابع تعریف کنیم. مثلا یک کار غیر همزمان! بعد از انجام کار اگر resolve را صدا بزنیم به این معنی است که آن کار به درستی انجام شده است و اگر reject را صدا بزنیم به معنی نقص در انجام آن کار است. همانند throw در جاوا اسکریپت، صدا کردن reject با آبجکت Error اختیاری است. مزیت استفاده از آبجکت Error این است که بهتر می‌توان کد را رصد و رفع باگ کرد.

حال که Promise را تعریف کردیم می‌توانیم به شکل زیر از آن استفاده کنیم:

promise.then(function(result) {
  console.log(result); // "kar anjam shod!"
}, function(err) {
  console.log(err); // Error: "kar dorost anjam nashod"
});

متد then() دو ورودی می‌گیرد. یک تابع callback برای وقتی که نتیجه Promise موفق است و یک تابع callback دیگر برای وقتی که خطا رخ می‌دهد. هر دوی این ورودی‌ها اختیاری هستند. بنابراین می‌توانیم فقط یک callback برای حالت موفق یا یک تابع برای حالت خطا استفاده کنیم.

شروع Promiseها با نام Futureها در DOM بود که بعدا به این نام تغییر پیدا کرد و نهایتا به جاوااسکریپت منتقل شد. بودن این ویژگی در جاوااسکریپت به جای DOM این مزیت بزرگ را دارد که در محیط‌های جاوااسکریپتی غیر از مرورگر، مثلا Node.js نیز می‌تواند به کار گرفته شود. (البته این‌که Promise در هسته‌ی این محیط‌ها تعریف شده باشد یا نه موضوع دیگری است).

سازگاری با کتابخانه‌های دیگر

Promise در جاوااسکریپت به گونه است که هر چیز شبه-Promise را که دارای متد then() است پوشش می‌دهد (همان tenableها). بنابراین اگر از کتابخانه‌ای استفاده می‌کنید که مثلا یک Promise مربوط به Q را برمی‌گرداند، مشکلی وجود نخواهد داشت و این موضوع با Promise جاوااسکریپت کاملا سازگار است. مثلا در مورد deferredهای jQuery می‌توان آن‌ها را سریع به Promiseهای جاوااسکریپت تبدیل کرد:

var jsPromise = Promise.resolve($.ajax('/whatever.json'));

در این‌جا $.ajax یک عمل diferred برمی‌گرداند. اما چون دارای متد then() است، Promise.resovle() می‌تواند آن را به یک Promise جاوااسکریپتی تبدیل کند. برخی اوقات diferredها مقادیر مختلفی را به عنوان ورودی callback می‌گیرند؛ برای مثال:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

که در این حالت Promise جاوااسکریپت همه آن‌ها را نادیده گرفته و فقط پارامتر اولی را در نظر می‌گیرد:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

خوشبختانه معمولا کار ما راه می‌افتد. همچنین این را بدانید که jQuery، ریجکت کردن با استفاده از آبجکت Error را پشتیبانی نمی‌کند.

Promise، کدنویسی غیر همزمان پیچیده را آسان می‌کند

بسیار خب! بیایید دست به کد شویم:

  • یک چرخانک به عنوان نشان دهنده لود شدن محتوا ظاهر کنیم،
  • یک فایل JSON را لود کنیم که شامل عنوان و آدرس URL هر فصل از یک کتاب است،
  • عنوان کتاب را اضافه کنیم،
  • هر فصل کتاب را لود کنیم،
  • فصل‌ها را به صفحه اضافه کنیم و
  • چرخانک لود شدن صفحه را برداریم.

… و البته اگر در این بین مشکلی به وجود آمد به کاربر اطلاع بدهیم که در این صورت هم می‌خواهیم که چرخانک لود برداشته شود. در غیر این صورت به چرخیدن ادامه می‌دهد و کاربر را گمراه می‌کند.

برای شروع بیایید داده خود را از شبکه لود کنیم:

تبدیل XMLHttpRequest به Promise

اگر APIها مشکل پشتیبانی از کدهای قدیمی را نداشته باشند، باید به تدریج به روز رسانی شوند تا از Promiseها پشتیبانی کنند. XMLHttpRequest نامزد شماره یک است! با این حال تا آن زمان باید خودمان دست به کار شویم و تابعی بنویسیم که درخواست GET را بگیرد:

function get(url) {
  // Yek Promise jadid barmigardanad.
  return new Promise(function(resolve, reject) {
    // Ijade darkhast:
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // In tabe hatta dar zamane error
      // ham farakhani mishavad.
      // pas status ra check mikonim:
      if (req.status == 200) {
        // Promise ra ba meghdare daryaft
        // shode resolve mikonim:
        resolve(req.response);
      }
      else {
        // dar gheyre in soorat Promise ra
        // ba matne error reject mikonim
        reject(Error(req.statusText));
      }
    };

    // Aghar khataye shabake vojud dashte
    // bashad ham reject mikonim:
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Darkhast ra miferestim:
    req.send();
  });
}

حالا بیایید از این Promise استفاده کنیم:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
});

حالا دیگر می‌توانیم بدون تایپ کردن هرباره‌ی XMLHttpRequest درخواست‌های HTTP ارسال کنیم. این عالی است! چون ندیدن قیافه‌ی چپ اندر قیچی XMLHttpRequest با آن حروف کوچک و بزرگ، زندگی را زیباتر می‌کند.

زنجیربافی یا Chaining

متد then() پایان ماجرا نیست. می‌توانیم زنجیره‌ای از then()ها تولید کنیم که داده‌های تولید شده در هر مرحله را با خود به مرحله بعدی می‌برند تا بتوان در آن مرحله استفاده کرده یا در کد غیر همزمان دیگری مورد استفاده قرار داد. به این عمل زنجیربافی می‌گوییم.

عمل حمل مقادیر

گفتیم یکی از اهداف زنجیربافی انتقال مقادیر تولید شده در طول زنجیره است. بیایید مثال بزنیم:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

در خط ۱، Promiseای را تعریف می‌کنیم که به مقدار «۱» resolve می‌شود. حال اگر متد then() را روی این Promise اجرا کنیم مقداری که از callback آن عبور می‌کند (مقدار val در خط ۵)، باید حامل عدد یک باشد. در ادامه به آن عدد ۲ را می‌افزاییم (خط ۷) و آن را return می‌کنیم. در ادامه اگر متد then() دیگری فراخوانده شود باید حامل عدد ۳ باشد. بیایید مفهوم بالا را روی پروژه‌ای که تعریف کردیم پیاده‌سازی کنیم:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

داده بدست آمده از فایل JSON از نوع JSON است. اما در حال حاضر به صورت متنی به دست ما می‌رسد. برای این‌که خروجی JSON بگیریم می‌توانیم تابعی که نوشتیم را تغییر دهیم و responseType را برابر JSON قرار دهیم. اما بیایید با استفاده از زنجیربافی این مشکل را حل کنیم:

get('story.json').then(function(response) {
  return JSON.parse(response); // tabdil be JSON
}).then(function(response) {
  console.log(response); //natije be shekle JSON ast.
})

در اینجا تابع ما برای گرفتن فایل، اجرا می‌شود و بعد داده آن در متد then() تحت عنوان response تزریق می‌شود. ما آن را گرفته و تبدیل به JSON می‌کنیم و دوباره برمی‌گردانیم. حال در متد then() بعدی، متغیر response خروجی JSON به ما می‌دهد.

چون JSON.parse() تنها یک مقدار می‌گیرد و مقدار را تغییر داده و بلافاصله برمی‌گرداند، می‌توانیم کد را کوتاه‌تر و به شکل زیر بنویسیم:

get('story.json').then(JSON.parse).then(function(response) {
  console.log(response); //natije be shekle JSON ast.
})

پس اگر بخواهیم تابع get() خود را طوری تغییر دهیم که خروجی JSON به ما بدهد، خیلی ساده می‌نویسیم:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

تابع getJSON() همچنان دارد یک Promise برمی‌گرداند. در واقع پس از اجرای هر متد then() دوباره چیزی که داریم یک Promise است.

صف‌بندی روندهای غیر همزمان

زنجیربافی متدهای then() را برای صف‌بندی یک تعداد رویداد غیر همزمان نیز می‌توانیم به کار ببریم. بنابراین عمل return از متد then() یک عمل جادویی محسوب می‌شود. به این صورت که اگر یک مقدار را بازگشت دهیم متد then() بعدی فراخوانی می‌شود و حاوی آن مقدار است؛ اما اگر چیزی شبیه Promise را بازگشت دهیم متد then() بعدی منتظر می‌شود تا آن Promise در وضعیت Settle قرار بگیرد؛ به این معنی که موفق شود یا شکست بخورد؛ و بعد فراخوانی می‌شود. مثال زیر موضوع را روشن می‌کند:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

فایل JSON را که یادتان هست؟ حاوی عنوان و لینک فصل‌های کتاب بود. کاری که انجام داده‌ایم این است که فایل JSON را فراخوانی کرده‌ایم. این یک عمل غیر همزمان است. پس متد then() در خط ۱ منتظر می‌ماند. بعد از تکمیل شدن درخواست، این متد فراخوانی می‌شود و متغیر story حاوی JSON است که لینک فصول را در خود داراست. از این then() یک عمل غیر همزمان دیگر را return می‌کنیم که همان لود کردن فصل اول است. این عمل نیز غیر همزمان است. پس then() بعدی در خط ۳ نیز منتظر تکمیل شدن می‌ماند و نهایتا همراه با مقدار chapter1 که حاوی فصل اول کتاب است اجرا می‌شود.

حال بیایید برای گرفتن فصول کتاب تابعی را تعریف کنیم:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// va be shekle zir estefade mikonim:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

story.json را تا زمان استفاده دانلود نمی‌کنیم. با فراخوانی getChapter()، فایل story.json تنها یکبار دانلود می‌شود (خط ۴) و در فراخوانی‌های مجدد تابع مذکور، دیگر فایل را دانلود نمی‌کنیم. این قدرت Promiseها است!

در بخش بعدی مقاله در مورد Error Handling و گرفتن خطاها صحبت کرده و پروژه تعریفی خود را قدم به قدم جلو می‌بریم. اگر می‌خواهید به اصل مقاله دسترسی داشته باشید اینجا کلیک کنید. با من همراه باشید.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *