تاریخ انتشار: چهارشنبه, ۳۱ ارديبهشت ۱۳۹۹، ۱۰:۳۷ ب.ظ نویسنده: محمدیان

به دلیل اینکه با مباحث مهمی روبرو هستیم، سعی کردم یه منبع پیدا کنم و راجع این مباحث یه مرجع کامل تر داشته باشیم. توجه کنید که ترتیب مباحث از ابتدای صفحه تا انتهای صفحه ست. اول callback رو بفمید و سپس promise و در آخر async و await .

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

ناهمگامی در زبان‌های برنامه‌نویسی

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

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

به طور معمول زبان‌های برنامه‌نویسی همگام هستند. برخی از آن‌ها نیز در حد زبان یا کتابخانه‌های جانبی، روش‌هایی برای مدیریت ناهمگامی ارائه می‌کنند. زبان‌های C ،Java ،C# ،PHP ،Go ،Ruby ،Swift و Python همگی به صورت پیش‌فرض همگام هستند. برخی از این زبان‌ها با استفاده از نخ‌ها به صورت ناهمگام عمل می‌کنند و می‌توانند یک پردازش جدید ایجاد کنند.

جاوا اسکریپت

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

const a = 1const b = 2const c = a * bconsole.log(c)doSomething()

اما می‌دانیم که جاوا اسکریپت درون مرورگر تولد یافته است و وظیفه اصلی آن در ابتدا پاسخ دادن به واکنش‌های کاربر مانند onClick ،onMouseOver ،onChange ،onSubmit و غیره بوده است. چنین فرایندی در یک مدل برنامه‌نویسی همگام چگونه ممکن است؟

پاسخ در محیط برنامه‌نویسی این زبان است. مرورگر روشی برای ارائه یک مجموعه از API-ها دارد که می‌توانند این نوع کارکرد را مدیریت کنند. در سال‌های اخیر Node.js یک محیط غیر مسدودکننده I/O معرفی کرده است که این مفهوم را به دسترسی فایل‌ها، فراخوانی‌های شبکه و موارد مشابه گسترش داده است.

Callback-ها

ما هرگز نمی‌دانیم که یک کاربر چه زمانی روی یک دکمه کلیک خواهد کرد، بنابراین باید یک «دستگیره رویداد» (event handler) برای رویداد کلیک تعریف کنیم.

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

document.getElementById('button').addEventListener('click'، () => {//item clicked})

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

رویه معمول این است که کد کلاینت در یک شنونده رویداد load در شیء window قرار می‌گیرد و تابع callback را در زمان آماده شدن صفحه اجرا می‌کند:

	
window.addEventListener('load'، () => {//window loaded //do what you want}

Callback-ها در همه جا استفاده می‌شوند و اختصاص به رویدادهای DOM ندارند. یک مثال عمومی استفاده از تایمر است:

setTimeout(() => {// runs after 2 seconds}، 2000

درخواست‌های XHR نیز یک callback می‌پذیرند. در مثال زیر یک تابع به مشخصه‌ای که در زمان رخداد اتفاق خاص فراخوانی می‌شود، انتساب خواهد یافت:

const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
        xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
    }
}
xhr.open('GET', 'https://yoursite.com') xhr.send()

مدیریت خطا در Callback-ها

یکی از رایج‌ترین راهبردها برای مدیریت خطا در callback-ها کاری است که Node.js انجام می‌دهد. پارامتر اول در هر تابع callback شیء خطا است که به نام callback-های error-first شناخته می‌شوند.

اگر خطایی وجود نداشته باشد، این شیء null خواهد بود. اگر خطایی رخ داده باشد این پارامتر نوعی توضیح در مورد خطا و اطلاعات مرتبط ارائه می‌کند.

fs.readFile('/file.json'، (err، data) => { if (err!== null) {//handle error console.log(err) return }

مشکل Callback-ها

به جای استفاده از کال بک میتوانیم کد خود را با پرامیس بنویسیم و یا کدهایی که به صورت کال بک نوشته شده تبدیل به پرامیس کنیم تا بتوانیم در آینده از مزایای async/await هم استفاده کنیم

Callback-ها در استفاده‌های ساده بسیار عالی هستند. اما هر Callback یک سطح به سلسله‌مراتب تودرتو اضافه می‌کند. زمانی که Callback-های زیادی وجود داشته باشند، کد به سرعت پیچیده می‌شود:

window.addEventListener('load'،() => {
    document.getElementById('button').addEventListener('click'،() => {
        setTimeout(() => {
                items.forEach(item => { //your code here
                })
            }،
            2000)
    })
})

این کد صرفاً 4 سطح ساده دارد، اما در موارد زیادی سطوح بسیار بیشتری از کد تودرتو را مشاهده می‌کنیم که جالب نیست. برای حل این مشکل چه باید کرد:

جایگزین‌های Callback-ها

از ES6 به بعد جاوا اسکریپت چند قابلیت معرفی کرده است که به کدنویسی ناهمگام بدون استفاده از Callback-ها کمک می‌کند:

  • Promises (ES6)
  • Async/Await (ES8)

Promise-ها

Promise-ها یک روش برای اجرای کد ناهمگام در جاوا اسکریپت محسوب می‌شوند و با استفاده از آن‌ها دیگر لازم نیست نگران وجود Callback-های زیاد در کد باشیم.

مقدمه‌ای بر Promise-ها

یک Promise عموماً به صورت واسطی برای یک مقدار تعریف می‌شود که در زمانی در آینده موجود خواهد شد. با این که سال‌هاست Promise-ها معرفی شده‌اند اما صرفاً در ES2015 استاندارد و معرفی شدند و هم اکنون در ES2017 با معرفی تابع‌های async منسوخ گشته‌اند. تابع‌های async از API مربوط به Promise ها به عنوان بلوک‌های سازنده خود استفاده می‌کند و از این رو درک آن‌ها حتی در صورتی که بخواهید در کد خود از تابع‌های async به جای promise استفاده کنید ضروری خواهد بود.

طرز کار Promise-ها به طور خلاصه

زمانی که یک Promise فراخوانی می‌شود در «حالت انتظار» (pending state) کار خود را آغاز می‌کند. این بدان معنی است که تابع فراخواننده به اجرای خود ادامه می‌دهد در حالی که منتظر Promise است تا پردازش‌های خودش را اجرا کند و بازخوردی به تابع فراخواننده بدهد.

در این زمان تابع فراخواننده منتظر آن می‌ماند تا Promise را در «حالت حل شده» (resolved state) یا در «حالت رد شده» (rejected state) بازگشت دهد، اما چنان که می‌دانید جاوا اسکریپت همگام است و از این رو در زمانی که Promise کار نمی‌کند، تابع همچنان به اجرای خود ادامه می‌دهد.

کدام API جاوا اسکریپت از Promise-ها استفاده می‌کند؟

علاوه بر کد کاربر و کد کتابخانه‌های مختلف، Promise-ها از سوی API-های مدرن استاندارد وب مانند لیست زیر نیز مورد استفاده قرار می‌گیرند:

  • Battery API
  • Fetch API
  • Service Workers

این که در جاوا اسکریپت مدرن از Promise-ها استفاده نکنید امر بسیار نامحتملی است، بنابراین در ادامه به بررسی دقیق‌تر آن‌ها می‌پردازیم.

ایجاد یک Promise

API مربوط به Promise یک سازنده Promise عرضه می‌کند که با استفاده از ()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)
    }
})

چنان که می‌بینید Promise ثابت سراسری done را بررسی می‌کند و اگر درست باشد، یک Promise حل شده و در غیر این صورت Promise رد شده بازگشت می‌یابد. ما با استفاده از resolve و reject می‌توانیم یک مقدار را بازگشت دهیم. در حالت فوق ما صرفاً یک رشته بازگشت می‌دهیم اما می‌توان یک شیء را نیز بازگشت داد.

مصرف کردن یک Promise

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

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

 

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

ناهمگامی در زبان‌های برنامه‌نویسی

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

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

به طور معمول زبان‌های برنامه‌نویسی همگام هستند. برخی از آن‌ها نیز در حد زبان یا کتابخانه‌های جانبی، روش‌هایی برای مدیریت ناهمگامی ارائه می‌کنند. زبان‌های C ،Java ،C# ،PHP ،Go ،Ruby ،Swift و Python همگی به صورت پیش‌فرض همگام هستند. برخی از این زبان‌ها با استفاده از نخ‌ها به صورت ناهمگام عمل می‌کنند و می‌توانند یک پردازش جدید ایجاد کنند.

جاوا اسکریپت

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

const a = 1const b = 2const c = a * bconsole.log(c)doSomething()

اما می‌دانیم که جاوا اسکریپت درون مرورگر تولد یافته است و وظیفه اصلی آن در ابتدا پاسخ دادن به واکنش‌های کاربر مانند onClick ،onMouseOver ،onChange ،onSubmit و غیره بوده است. چنین فرایندی در یک مدل برنامه‌نویسی همگام چگونه ممکن است؟

پاسخ در محیط برنامه‌نویسی این زبان است. مرورگر روشی برای ارائه یک مجموعه از API-ها دارد که می‌توانند این نوع کارکرد را مدیریت کنند. در سال‌های اخیر Node.js یک محیط غیر مسدودکننده I/O معرفی کرده است که این مفهوم را به دسترسی فایل‌ها، فراخوانی‌های شبکه و موارد مشابه گسترش داده است.

Callback-ها

ما هرگز نمی‌دانیم که یک کاربر چه زمانی روی یک دکمه کلیک خواهد کرد، بنابراین باید یک «دستگیره رویداد» (event handler) برای رویداد کلیک تعریف کنیم.

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

document.getElementById('button').addEventListener('click'، () => {//item clicked})

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

رویه معمول این است که کد کلاینت در یک شنونده رویداد load در شیء window قرار می‌گیرد و تابع callback را در زمان آماده شدن صفحه اجرا می‌کند:

	
window.addEventListener('load'، () => {//window loaded //do what you want}

Callback-ها در همه جا استفاده می‌شوند و اختصاص به رویدادهای DOM ندارند. یک مثال عمومی استفاده از تایمر است:

setTimeout(() => {// runs after 2 seconds}، 2000

درخواست‌های XHR نیز یک callback می‌پذیرند. در مثال زیر یک تابع به مشخصه‌ای که در زمان رخداد اتفاق خاص فراخوانی می‌شود، انتساب خواهد یافت:

const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
        xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
    }
}
xhr.open('GET', 'https://yoursite.com') xhr.send()

مدیریت خطا در Callback-ها

یکی از رایج‌ترین راهبردها برای مدیریت خطا در callback-ها کاری است که Node.js انجام می‌دهد. پارامتر اول در هر تابع callback شیء خطا است که به نام callback-های error-first شناخته می‌شوند.

اگر خطایی وجود نداشته باشد، این شیء null خواهد بود. اگر خطایی رخ داده باشد این پارامتر نوعی توضیح در مورد خطا و اطلاعات مرتبط ارائه می‌کند.

fs.readFile('/file.json'، (err، data) => { if (err!== null) {//handle error console.log(err) return }

مشکل Callback-ها

Callback-ها در استفاده‌های ساده بسیار عالی هستند. اما هر Callback یک سطح به سلسله‌مراتب تودرتو اضافه می‌کند. زمانی که Callback-های زیادی وجود داشته باشند، کد به سرعت پیچیده می‌شود:

window.addEventListener('load'،() => {
    document.getElementById('button').addEventListener('click'،() => {
        setTimeout(() => {
                items.forEach(item => { //your code here
                })
            }،
            2000)
    })
})

این کد صرفاً 4 سطح ساده دارد، اما در موارد زیادی سطوح بسیار بیشتری از کد تودرتو را مشاهده می‌کنیم که جالب نیست. برای حل این مشکل چه باید کرد؟

جایگزین‌های Callback-ها

از ES6 به بعد جاوا اسکریپت چند قابلیت معرفی کرده است که به کدنویسی ناهمگام بدون استفاده از Callback-ها کمک می‌کند:

  • Promises (ES6)
  • Async/Await (ES8)

Promise-ها

Promise-ها یک روش برای اجرای کد ناهمگام در جاوا اسکریپت محسوب می‌شوند و با استفاده از آن‌ها دیگر لازم نیست نگران وجود Callback-های زیاد در کد باشیم.

مقدمه‌ای بر Promise-ها

یک Promise عموماً به صورت واسطی برای یک مقدار تعریف می‌شود که در زمانی در آینده موجود خواهد شد. با این که سال‌هاست Promise-ها معرفی شده‌اند اما صرفاً در ES2015 استاندارد و معرفی شدند و هم اکنون در ES2017 با معرفی تابع‌های async منسوخ گشته‌اند. تابع‌های async از API مربوط به Promise ها به عنوان بلوک‌های سازنده خود استفاده می‌کند و از این رو درک آن‌ها حتی در صورتی که بخواهید در کد خود از تابع‌های async به جای promise استفاده کنید ضروری خواهد بود.

طرز کار Promise-ها به طور خلاصه

زمانی که یک Promise فراخوانی می‌شود در «حالت انتظار» (pending state) کار خود را آغاز می‌کند. این بدان معنی است که تابع فراخواننده به اجرای خود ادامه می‌دهد در حالی که منتظر Promise است تا پردازش‌های خودش را اجرا کند و بازخوردی به تابع فراخواننده بدهد.

در این زمان تابع فراخواننده منتظر آن می‌ماند تا Promise را در «حالت حل شده» (resolved state) یا در «حالت رد شده» (rejected state) بازگشت دهد، اما چنان که می‌دانید جاوا اسکریپت همگام است و از این رو در زمانی که Promise کار نمی‌کند، تابع همچنان به اجرای خود ادامه می‌دهد.

کدام API جاوا اسکریپت از Promise-ها استفاده می‌کند؟

علاوه بر کد کاربر و کد کتابخانه‌های مختلف، Promise-ها از سوی API-های مدرن استاندارد وب مانند لیست زیر نیز مورد استفاده قرار می‌گیرند:

  • Battery API
  • Fetch API
  • Service Workers

این که در جاوا اسکریپت مدرن از Promise-ها استفاده نکنید امر بسیار نامحتملی است، بنابراین در ادامه به بررسی دقیق‌تر آن‌ها می‌پردازیم.

ایجاد یک Promise

API مربوط به Promise یک سازنده Promise عرضه می‌کند که با استفاده از ()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)
    }
})

چنان که می‌بینید Promise ثابت سراسری done را بررسی می‌کند و اگر درست باشد، یک Promise حل شده و در غیر این صورت Promise رد شده بازگشت می‌یابد. ما با استفاده از resolve و reject می‌توانیم یک مقدار را بازگشت دهیم. در حالت فوق ما صرفاً یک رشته بازگشت می‌دهیم اما می‌توان یک شیء را نیز بازگشت داد.

مصرف کردن یک Promise

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

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

اجرای ()checkIfItsDone موجب اجرا شدن یک Promise به نام ()isItDoneYet می‌شود که منتظر حل شدن با استفاده از Callback-ی به نام then باقی می‌ماند و اگر خطایی رخ دهد آن را در callback با نام catch مدیریت می‌کند.

زنجیره‌سازی Promise-ها

هر Promise می‌تواند به Promise دیگری بازگشت یابد و یک زنجیره از Promise-ها تشکیل دهد. مثالی عالی از زنجیره‌سازی Promise-ها در API با نام Fetch مشاهده می‌شود که یک لایه فوقانی روی API مربوط به XMLHttpRequest است و می‌توان از آن برای ایجاد یک منبع و صف‌بندی یک زنجیره از Promise-ها برای اجرا در زمان واکشی شدن منبع استفاده کرد.

API با نام Fetch یک سازوکار مبتنی بر Promise است به طوری که فراخوانی ()fetch معادل تعریف کردن یک Promise با استفاده از ()new 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)
})

در این مثال، ()fetch را فراخوانی می‌کنیم تا فهرستی از آیتم‌های TODO را از فایل todos.json که در ریشه دامنه قرار دارد به دست آوریم و بنابراین یک زنجیره از Promise-ها ایجاد کرده‌ایم.

اجرای ()fetch موجب بازگشت یک response می‌شود که مشخصه‌های زیادی دارد، اما دو مورد از آن‌ها برای ما مهم هستند:

  • Status – یک مقدار عددی است که کد وضعیت HTTP را نشان می‌دهد.
  • statusText – یک پیام وضعیت است که در صورت موفق بودن درخواست، مقدار آن OK خواهد بود.

response یک متد ()json نیز دارد که یک Promise بازگشت می‌دهد و در آن به بررسی محتوای متنی پردازش شده و تبدیل آن به JSON می‌پردازد. با توجه به Promise-های فوق روند رویدادها به این صورت است که Promise اول در زنجیره، تابعی است که ما تعریف کرده‌ایم و آن را ()status می‌نامیم. این Promise وضعیت پاسخ را بررسی می‌کند و در صورتی که پاسخ موفق (کد بین 200 تا 299) نباشد، Promise را رد می‌کند.

این عملیات موجب می‌شود که زنجیره Promise همه Promise-های بعدی لیست شده را رد کند و مستقیماً به گزاره ()catch در انتهای کد برسد و متن Request failed را به همراه پیام خطا لاگ کند. اما اگر Promise اول موفق باشد، تابع ()json را که تعریف کرده‌ایم، فراخوانی می‌کند. از آنجا که Promise قبلی در صورت موفقیت، شیء response را بازگشت داده است، می‌توانیم آن را به عنوان ورودی به Promise دوم بدهیم.

در این حالت، داده‌های JSON پردازش شده را بازگشت می‌دهیم تا Promise سوم مستقیماً JSON را دریافت کند:

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

در نهایت آن را در کنسول لاگ می‌کنیم.

مدیریت خطاها

در مثال بخش قبلی، یک catch داشتیم که به زنجیره Promise-ها الصاق می‌شد. هر زمان که جزئی از زنجیره Promise-ها ناموفق باشد و خطایی رخ دهد یا یک Promise رد شود، کنترل به گزاره ()catch در انتهای زنجیره می‌رسد.

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

یا

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 می‌پردازیم.

()Promise.all

اگر لازم است که Promise-های مختلف را با یکدیگر هماهنگ کنید، می‌توانید از متد ()Promise.all برای تعریف کردن لیستی از 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)
})

ساختار destructuring assignment در ES2015 نیز اجازه اجرای چنین کاری را می‌دهد:

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

البته شما محدود به استفاده از fetch نیستید و می‌توانید از هر Promise دیگری نیز استفاده کنید.

()Promise.race

این متد زمانی اجرا می‌شود که اولین Promise که به آن ارسال کرده‌اید حل شود و callback الصاق شده به آن را یک بار به همراه نتیجه نخستین 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}

یک خطای رایج

اگر با خطای زیر در کنسول مواجه شدید:

Uncaught TypeError: undefined is not a promise

ابتدا باید اطمینان حاصل کنید که به جای ()Promise از ()new Promise استفاده کرده‌اید.

Async و Await

جدیدترین رویکرد به تابع‌های ناهمگام در جاوا اسکریپت Async و Await است. جاوا اسکریپت در طی مدت بسیار کوتاهی (ES2015) استفاده از Callback را کنار گذاشت و به بهره‌گیری از Promise-ها روی آورد. از نسخه ES2017 نیز کدهای ناهمگام در جاوا اسکریپت با استفاده از ساختار async/await ساده‌تر شده‌اند.

تابع‌های async ترکیبی از Promise-ها و generatos-ها هستند و اساساً لایه بالاتری از تجرید نسبت به Promise محسوب می‌شوند. در اینجا باید یک بار دیگر تکرار کنیم که async/await بر مبنای Promise-ها ساخته شده‌اند.

Async/Await چرا معرفی شد؟

تابع‌های Async/Await باعث کاهش حجم کد مورد نیاز برای نوشتن Promise-ها می‌شوند و محدودیت «عدم قطع زنجیره» Promise-های زنجیره‌سازی را نیز مرتفع می‌سازند. زمانی که Promise-ها در نسخه ES2015 معرفی شدند، هدف از ارائه آن‌ها حل مشکلی در کدنویسی ناهمگام بود و در این کار نیز موفق بودند، اما در طی دو سال که بین عرضه نسخه‌های ES2015 و ES2017 وجود داشت مشخص شد که Promise-ها نمی‌توانند راه‌حل نهایی باشند.

Promise-ها برای حل مشکل مشهور جهنم callback-ها عرضه شدند، اما آن‌ها نیز پیچیدگی خاص خود را داشتند و از نظر کاربران دارای ساختار پیچیده‌ای بودند. Promise-ها با این کارکرد که به ما نشان دادند می‌توان از ساختار مناسب‌تری به این منظور استفاده کرد مفید بودند و لذا اینک زمانی فرا رسیده است که از تابع‌های async استفاده کنیم. تابع‌های async/await موجب می‌شوند کد همگام به نظر برسد، اما اساساً ناهمگام است و در پشت صحنه به روشی غیر مسدودکننده عمل می‌کند.

طرز کار Async/Await چگونه است؟

تابع async یک Promise بازگشت می‌دهد. به مثال زیر توجه کنید:

const doSomethingAsync = () => {
    return new Promise((resolve) => {
        setTimeout(() => resolve('I did something')، 3000)
    })
}

زمانی که بخواهیم این تابع را فراخوانی کنیم یک await آماده می‌کنیم و کد فراخوانی کننده متوقف می‌شود تا این که Promise حل یا رد شود. تنها الزام این است که تابع کلاینت باید به صورت async تعریف شود. به مثال زیر توجه کنید:

const doSomething = async () => {
    console.log(await doSomethingAsync())
}

یک مثال ساده

در مثال ساده زیر از async/await برای اجرای ناهمگام یک تابع استفاده شده است:

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

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

BeforeAfterI did something //after 3s

Promise کردن همه چیز

الصاق کردن کلیدواژه async به هر تابعی به این معنی است که تابع یک Promise بازگشت خواهد داد. حتی اگر این کار به طور صریح انجام نشده باشد، این کار موجب می‌شود که تابع به صورت درونی یک Promise بازگشت دهد. به همین دلیل است که کد زیر معتبر است:

const aFunction = async () => { return 'test'}
aFunction().then(alert) // This will alert 'test'

این کد نیز مانند کد فوق است:

const aFunction = async () => { return Promise.resolve('test')}
aFunction().then(alert) // This will alert 'test

خواندن این کد کاملاً آسان‌تر است

چنان که در مثال فوق می‌بینیم، کد ما بسیار ساده به نظر می‌رسد. آن را با کدی که از Promise-های ساده استفاده می‌کرد و از زنجیره‌سازی و تابع‌های callback بهره می‌گرفت مقایسه کنید. این یک مثال بسیار ساده است، مزیت اصلی زمانی بروز می‌یابد که کد بسیار پیچیده‌تر باشد. برای نمونه با استفاده از Promise-ها به صورت زیر می‌توان یک منبع JSON را دریافت کرده و آن را تحلیل کرد:

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 => response.json()) parse JSON
}

getFirstUserData()

کد زیر همان کارکرد فوق را با استفاده از await/async اجرا می‌کند:

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 user.json() parse JSON
    return userData
}

getFirstUserData()

سری کردن چند تابع Async

تابع‌های Async را می‌توان بسیار ساده به هم زنجیر کرد و ساختار آن نسبت به Promise-های ساده بسیار خواناتر است:

const promiseToDoSomething = () => {
    return new Promise(resolve => {
        setTimeout(() => resolve('I did something'), 10000)
    })
}

const watchOverSomeoneDoingSomething = async () => {
    const something = await promiseToDoSomething() 
    return something + ' and I watched'
}

const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
    const something = await watchOverSomeoneDoingSomething()
    return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => { console.log(res)})

کد فوق عبارت زیر را نمایش می‌دهد:

I did something and I watched and I watched as well

دیباگ آسان‌تر

دیباگ کردن Promise-ها دشوار است زیرا دیباگر روی کد ناهمگام نمی‌ایستد. اما async/await کار دیباگ را بسیار آسان‌تر ساخته‌اند، زیرا از نظر کامپایلر آن‌ها کد همگام هستند.

Event Emitter در Node.js

با استفاده از Event Emitter در Node.js می‌توان رویدادهای سفارشی ایجاد کرد. اگر با جاوا اسکریپت در مرورگر کار کرده باشید، می‌دانید که بخش عمده تعامل با کاربر از طریق رویدادهایی مانند کلیک‌های ماوس، فشردن کلیدهای کیبورد، واکنش به حرکت ماوس و غیره مدیریت می‌شود. در سمت بک‌اند، Node.js کلاس EventEmitter را ارائه کرده است که برای مدیریت رویدادها استفاده می‌شود. این کلاس را می‌توان با استفاده از کد زیر مقداردهی کرد:

const eventEmitter = require('events').EventEmitter()

این شیء متدهای on و emit برخی متدهای دیگر را عرضه می‌کند.

  • emit – برای تحریک کردن یک رویداد استفاده می‌شود.
  • on – برای افزودن تابع callback استفاده می‌شود که قرار است هنگام تحریک شدن رویداد اجرا شود.

برای نمونه، یک رویداد start ایجاد می‌کنیم و از آنجا که می‌خواهیم مثال ساده‌ای باشد، واکنش ما به آن لاگ کردن پیامی در کنسول است:

eventEmitter.on('start'، () => { console.log('started')})

زمانی که رویداد را اجرا کنیم:

eventEmitter.emit('start')

تابع دستگیره رویداد تحریک می‌شود و لاگ کنسول ایجاد می‌شود. با استفاده از آرگومان‌های اضافی ()emit می‌توان آرگومان‌هایی به دستگیره رویداد ارسال کرد:

eventEmitter.emit('start')
eventEmitter.emit('start'، 23)

آرگومان‌های چندگانه

eventEmitter.on('start'، (start، end) => { console.log(`started from ${start} to ${end}`)})
eventEmitter.emit('start'، 1، 100)

شیء EventEmitter چند متد دیگر نیز برای تعامل با رویدادها در اختیار ما قرار می‌دهد که از آن جمله‌اند:

  • ()once – یک شنونده یک‌بارمصرف اضافه می‌کند.
  • ()removeListener() / off – یک شنونده رویداد را از رویداد حذف می‌کند.
  • ()removeAllListeners – همه شنونده‌ها را از یک رویداد حذف می‌کند.

 

منبع: وبلاگ فرادرس