جاوااسکریپت Promise : چگونه در ES5 مشکل asynchronous حل شد؟

معمولا promise های جاوااسکریپت برای حل مشکل ناهمگامی (ناهمزمان – asynchronous ) مورد استفاده قرار میگیرند. پرامیس در اکما اسکریپت 2015 (ES5) رونمایی شده است. در مقاله بعدی درباره async/await صحبت خواهیم کرد. اما قبل از آن باید از کارکرد پرامیس به درستی آگاه شویم.

اینجا مقاله ای از دایکومنتیشن سایت رسمی ند جی اس را ترجمه میکنیم.

پرامیس ها در سال 2015 تا 2017 تنها راه حل برای برنامه نویسی ناهمگام بودند که از callback hell جلوگیری میکردند. تا اینکه در سال 2017 از async/await رونمایی شد.

هرگاه یک پرامیس اجرا میشود، به حالت انتظار در می آید. به این معنا که به اجرای خود ادامه میدهد تا زمانیکه به حالت resolved یا rejected برسد. سپس با حالت ریسولو یا ریجکت در then یا catch مورد بهره برداری قرار میگیرد.

پرامیس ها در Web APIs های مدرن و استاندارد مورد بهره برداری قرار میگیرند: Fetch API ، Battery API و Service Workers !

جاوااسکریپت promise : ساختن یک پرامیس

Promise API دارای یک constructor است که با new promise راه اندازی میشود.

let done = true

const isItDoneYet = new Promise((resolve, reject) => {
  if (done) {
    const workDone = 'Here is the thing I built'
    resolve(workDone)
  } else {
    const why = 'Still working on something else'
    reject(why)
  }
})

همانطور که مشاهده میکنید متغیر گلوبال done در isItDoneYet چک میشود و اگر مقدارش true باشد، پرامیس را با resolve به حالت resolved در می آورد. اگر مقدارش false بود انگاه به حالت rejected در می آمد (با متد reject) !

اگر هیچ یک از حالت های resolved یا rejected رخ ندهد، پرامیس به حالت قبلی خود (حالت انتظار – pending status) باقی میماند.

در مثال بالا workDone و why یک استرینگ هستند اما میتوانست ابجکت یا null هم باشند.

یک نمونه مثال دیگر با هم میبینیم. در مثال زیر از یک تکنیک به نام Promisifying رونمایی شده است. این تکنیک زمانی رخ میدهد که یک تابع کلاسیک جاوا اسکریپت (که کال بک فانکشن میپذیرد و آن را دارد)، یک پرامیس را برمیگرداند. آن تابع کلاسیک اینجا getFile است.

const fs = require('fs')

const getFile = (fileName) => {
  return new Promise((resolve, reject) => {
    fs.readFile(fileName, (err, data) => {
      if (err) { reject(err); return }
      resolve(data)
    })
  })
}

getFile('/etc/passwd')
.then(data => console.log(data))
.catch(err => console.error(err))

در ورژن های اخیر Node.js نباید این کار را برای بسیاری از API ها بصورت دستی انجام دهید. یک تابع که تکنیک Promisifying را بجای کدهای دستی شما برایتان انجام دهد در ماژول util وجود دارد.

استفاده کردن از پرامیس راه اندازی شده

در بخش قبل دیدیم چگونه پرامیس راه اندازی میشود. اینجا از پرامیس بهره برداری خواهیم کرد.

const isItDoneYet = new Promise(/* ... as above ... */)
//...

const checkIfItsDone = () => {
  isItDoneYet
    .then(ok => { console.log(ok) })
    .catch(err => { console.error(err) })
}

با اجرای checkIfItsDone و وارد شدن isItDoneYet به then یا catch خواهید دید که اگر isItDoneYet به حالت resolved درامده باشد به داخل then میرود و اگر به حالت rejected باشد در catch اجرا خواهد شد.

جاوااسکریپت promise : زنجیره پرامیس ها

پرامیس ها میتوانند یک پرامیس دیگر را برگردانند و زنجیره بسازند. یک مثال خوب میتواند درباره Fetch API باشد. میتوان داده های یک منبع را با متد GET کرد.

Fetch API بر اساس پرامیس ساخته شده است. استفاده از fetch برابر با تعریف new 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', data) })
  .catch(error => { console.log('Request failed', error) })

در این مثال ما از متد fetch استفاده میکنیم تا داده های داخل todos.json را دریافت کنیم. با متد fetch ما یک response دریافت میکنیم که پروپرتی های زیادی از جمله این پروپرتی ها دارد:

status : یک عدد است که HTTP status code را نشان میدهد.

statusText : یک پیام درباره status است. اگر درخواست HTTP موفقیت آمیز باشد این مقدار OK است.

response یک متد json هم دارد، که یک پرامیس را باز میگرداند (که با محتوای بدنه ی response که به فرمت جیسون تبدیل شده) resolve میشود.

بنابراین با وجود این پرامیس ها این اتفاق در کدهای بالا در حال رخ دادن است: اولین پرامیسی که در زنجیره ساختیم با عنوان status رخ نمایی میکند. در این تابع response.status چک میشود. چنانچه مقدارش بین 200 تا 300 نباشد، انگاه پرامیس reject میشود.

در این حالت rejected ، زنجیره از دو تا then رد شده و به catch میرسد. انگاه در کنسول Request failed به همراه توضیحاتی درباره ارور نمایش داده میشود.

اگر بجای حالت rejected حالت resolved رخ بدهد، زنجیره بعدی then رخ میدهد. json فراخوانده میشود. از آنجا که پرامیس قبلی موفقیت آمیز بوده بنابراین ابجکت response بازگردانده میشود و به عنوان ورودی json وارد این تابع میشود، و در انجا وارد پرامیس جدیدی میشود.

سپس داده ها به فرمت جیسون از پرامیس دوم خارج میشوند و به پرامیس سوم میروند تا در کنسول لاگ شوند. (کدهای زیر)

.then((data) => {
  console.log('Request succeeded with JSON response', data)
})

جاوااسکریپت promise : مدیریت ارور ها

در مثال قبل دیدیم یک متد catch به پایان زنجیره چسبیده بود.

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

new Promise((resolve, reject) => {
  throw new Error('Error')
}).catch(err => {
  console.error(err)
})

// or

new Promise((resolve, reject) => {
  reject('Error')
}).catch(err => {
  console.error(err)
})

آبشار ارور ها

اگر داخل catch باز هم یک ارور جدید رخ بدهد، میتوانید یک catch جدید برای مدیریت کردن ارور جدید تعریف کنید.

new Promise((resolve, reject) => {
  throw new Error('Error')
})
  .catch(err => {
    throw new Error('Error')
  })
  .catch(err => {
    console.error(err)
  })

جاوااسکریپت promise :هماهنگ کردن چند پرامیس با هم

Promise.all

اگر میخواهید چند پرامیس را با هم همزمان مدیریت کنید میتوانید از Promise.all استفاده کنید. وقتی همه ان پرامیس ها resolved بشوند انگاه چیزی اجرا خواهد شد:

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

اگر از ساختار بالا خوشتان نمیاد ES2015 به شما اجازه میدهد از قاعده نوشتاری زیر هم استفاده کنید:

Promise.all([f1, f2]).then(([res1, res2]) => {
  console.log('Results', res1, res2)
})

Promise.race

در Promise.all تمام پرامیس ها اجرا میشدند اما اگر میخواهید مسابقه بگذارید تا پرامیسی که زودتر resolve میشود اجرا شود و دیگری اجرا نشود میتوانید از Promise.race استفاده کنید.

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

ارور های متداول

Uncaught TypeError: undefined is not a promise

اگر کنسول به ارور Uncaught TypeError: undefined is not a promise برخورد کردید احتمالا بجای اینکه new Promise بنویسید از Promise خالی استفاده کرده اید.

UnhandledPromiseRejectionWarning

این ارور نشان میدهد که پرامیس شما reject شده اما هیچ catch وجود نداشته که ارور را مدیریت کند. لطفا بعد از then یک catch هم بنویسید!