Promise در جاوااسکریپت یک ابزار قدرتمند برای مدیریت عملیات asynchronous است و استفاده از آن، به ویژه در eventهای مرتبط با UI بسیار مفید می‌باشد.

Promiseها به توسعه‌دهندگان اجازه می‌دهند هنگام رسیدگی به کارهایی مانند API callها، تعاملات کاربر یا انیمیشن‌ها کد تمیزتر و قابل مدیریت‌تری بنویسند. با استفاده از متدهایی مانند .then()، .catch() و .finally()، promiseها روش بصری‌تر را برای مدیریت سناریوهای موفقیت و خطا و اجتناب از callback hell ارائه می‌دهند.

در این مقاله، از متد جدید Promise.withResolvers() استفاده خواهیم کرد که به ما این امکان را می‌دهد تا با return کردن یک آبجکت که حاوی سه مورد می‌باشد، کدهای تمیزتر و ساده‌تری بنویسیم. این سه مورد عبارتند از: یک promise جدید و دو تابع، یک تابع برای resolve کردن promise و دیگری برای reject کردن آن.

مقایسه متدهای Promise قدیمی و جدید در جاوااسکریپت

در دو قطعه کد زیر که از نظر فانکشنالیتی معادل هم هستند، ما می‌توانیم رویکرد قدیمی و رویکرد جدید تخصیص متد برای resolve یا reject یک promise را مقایسه کنیم:

let resolve, reject;

const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

Math.random() > 0.5 ? resolve("ok") : reject("not ok");

در کد بالا، می‌توانیم سنتی‌ترین کاربرد promise را ببینیم: یک آبجکت promise جدید می‌سازیم، سپس در constructor باید دو تابع resolve و reject را تخصیص دهیم که در صورت نیاز فراخوانی می‌شوند.

اکنون همان کد قبلی را با متد Promise.withResolvers() جدید بازنویسی می‌کنیم که به نظر می‌رسد ساده‌تر نیز می‌باشد:

const { promise, resolve, reject } = Promise.withResolvers();

Math.random() > 0.5 ? resolve("ok") : reject("not ok");

در این مثال می‌توانیم نحوه عملکرد رویکرد جدید را مشاهده کنیم. این رویکرد promise را return می‌کند، که در آن می‌توانیم متد .then() و دو تابع را فراخوانی کرده و آن را resolve و reject نماییم.

رویکرد سنتی نسبت به promise، منطق ایجاد و مدیریت event را در یک تابع واحد محصور می‌کند، که اگر شرط‌های متعدد داشته باشیم یا بخش‌های مختلف کد نیاز به resolve یا reject کردن promiseها داشته باشند، این موضوع می‌تواند محدودکننده باشد.

در مقابل، Promise.withResolvers() انعطاف‌پذیری بیشتری را با جدا کردن ساخت promise از منطق رزولوشن فراهم می‌کند، و آن را برای مدیریت شرط‌های پیچیده یا eventهای متعدد مناسب می‌کند. با این حال، برای موارد ساده‌تر، استفاده از روش سنتی ممکن است برای کسانی که به الگوهای promise استاندارد عادت دارند ساده‌تر و آشناتر باشد.

بررسی یک مثال واقعی: فراخوانی یک API

اکنون می‌توانیم رویکرد جدید را روی یک مثال واقعی‌تر بررسی نماییم. در کد زیر می‌توانیم یک مثال ساده از فراخوانی API را مشاهده کنیم:

function fetchData(url) {
    return new Promise((resolve, reject) => {
        fetch(url)
            .then(response => {
                // Check if the response is okay (status 200-299)
                if (response.ok) {
                    return response.json(); // Parse JSON if response is okay
                } else {
                    // Reject the promise if the response is not okay
                    reject(new Error('API Invocation failed'));
                }
            })
            .then(data => {
                // Resolve the promise with the data
                resolve(data);
            })
            .catch(error => {
                // Catch and reject the promise if there is a network error
                reject(error);
            });
    });
}

// Example usage
const apiURL = '<ADD HERE YOU API ENDPOINT>';

fetchData(apiURL)
    .then(data => {
        // Handle the resolved data
        console.log('Data received:', data);
    })
    .catch(error => {
        // Handle any errors that occurred
        console.error('Error occurred:', error);
    });

تابع fetchData برای گرفتن URL و return کردن یک promise طراحی شده است که یک API call را با استفاده از fetch API انجام می‌دهد. سپس response را با بررسی اینکه آیا وضعیت آن در محدوده ۲۰۰-۲۹۹ است یا خیر پردازش می‌کند که نشان‌دهنده موفقیت می‌باشد.

در صورت موفقیت‌آمیز بودن، response به عنوان parse JSON شده و promise با داده‌های به دست آمده resolve می‌شود. در صورت عدم موفقیت response، این بار promise با یک پیغام خطای مناسب reject می‌گردد. علاوه بر این، این تابع رسیدگی به خطاها برای شناسایی هر گونه خطای شبکه و reject کردن promise در صورت وقوع چنین خطایی را نیز شامل می‌شود.

این مثال نحوه استفاده از این تابع را نشان می‌دهد و ما نحوه مدیریت داده‌های resolve شده با یک بلاک .then() و مدیریت خطاها با استفاده از بلاک .catch() را مشاهده می‌کنیم و می‌بینیم که این قطعه کد، اطمینان حاصل می‌کند که هم بازیابی موفق داده‌ها و هم خطاها هر دو به درستی مدیریت می‌شوند.

در کد زیر، تابع fetchData() را با استفاده از متد جدید Promise.withResolvers() دوباره می‌نویسیم:

function fetchData(url) {
    const { promise, resolve, reject } = Promise.withResolvers();

    fetch(url)
        .then(response => {
            // Check if the response is okay (status 200-299)
            if (response.ok) {
                return response.json(); // Parse JSON if response is okay
            } else {
                // Reject the promise if the response is not okay
                reject(new Error('API Invocation failed'));
            }
        })
        .then(data => {
            // Resolve the promise with the data
            resolve(data);
        })
        .catch(error => {
            // Catch and reject the promise if there is a network error
            reject(error);
        });

    return promise;
}

همانطور که می‌بینیم، کد بالا خوانایی بیشتری دارد و نقش آبجکت promise در آن واضح‌تر است. تابع fetchData یک promise را return می‌کند که یا با موفقیت resolve می‌شود، یا با شکست مواجه می‌گردد و در هر مورد متد مناسب را فراخوانی می‌کند. می‌توانیم کد بالا را در این لینک مشاهده نماییم.

بررسی لغو Promise

کد زیر نحوه اجرای متد لغو promise را بررسی می‌کند. همانطور که می‌دانیم، ما نمی‌توانیم یک promise را در جاوااسکریپت لغو کنیم. Promiseها نتیجه یک عملیات asynchronous را نشان می‌دهند و به گونه‌ای طراحی شده‌اند که پس از ایجاد، resolve یا reject شوند، و هیچگونه مکانیزم داخلی برای لغو آن‌ها تعریف نشده است.

این محدودیت به این دلیل است که promiseها یک فرآیند انتقال state تعریف شده دارد. آن‌ها به صورت معلق شروع می‌شوند و پس از resolve شدن، نمی‌توانند state را تغییر دهند. آن‌ها به جای اینکه خود عملیات را کنترل کنند، نتیجه یک عملیات را محصور می‌کنند. به این معنی که promiseها نمی‌توانند بر روی روند اصلی تأثیری بگذارند یا آن را لغو نمایند. این انتخاب طراحی، promiseها را ساده‌تر کرده و آن‌ها را بر روی نمایش نتیجه نهایی یک عملیات متمرکز می‌کند:

const cancellablePromise = () => {
    const { promise, resolve, reject } = Promise.withResolvers();

    promise.cancel = () => {
        reject("the promise got cancelled");
    };
    return promise;
};

در کد بالا، می‌توانیم آبجکت با نام cancellablePromise را مشاهده کنیم، که یک promise با متد cancel() اضافی است. همانطور که می‌بینیم، این متد به شکل ساده فراخوانی متد reject را به صورت اجباری انجام می‌دهد. البته این فقط دستور سینتکسی است و یک promise جاوااسکریپت را لغو نمی‌کند، اما ممکن است به نوشتن کد واضح‌تر کمک کند.

یک رویکرد جایگزین، استفاده از AbortController و AbortSignal است که می‌تواند به عملیات اساسی (مثلاً یک درخواست HTTP) متصل شود تا در صورت نیاز آن را لغو کند. می‌توانیم در مستندات موجود مشاهده کنیم که رویکرد AbortController و AbortSignal پیاده‌سازی واضح‌تری نسبت به آنچه که ما در مثال بالا انجام دادیم، دارند: هنگامی که AbortSignal فراخوانی شود، promise ریجکت می‌شود.

رویکرد دیگری نیز وجود دارد که استفاده از کتابخانه‌های برنامه‌نویسی واکنش‌پذیر مانند RxJS است. این کتابخانه، اجرای الگوی Observable و کنترل پیچیده‌تر بر جریان‌های داده‌های async، از جمله قابلیت‌های لغو را ارائه می‌دهد.

مقایسه بین Observableها و Promiseها

به طور کلی، promiseها برای مدیریت عملیات asynchronous منفرد، مانند دریافت داده‌ها از یک API، مناسب هستند. در مقابل، observableها برای مدیریت جریان‌های داده، مانند ورودی کاربر، eventهای WebSocket یا پاسخ‌های HTTP، که در آن مقادیر متعددی ممکن است در طول زمان منتشر شوند، ایده‌آل می‌باشند.

همانطور که دیدیم promiseها پس از شروع، قابل لغو نیست، در حالی که observableها با انجام unsubscribing از جریان، امکان لغو را فراهم می‌کنند. ایده کلی این است که با استفاده از observableها، ما یک ساختار صریح از تعامل احتمالی با آبجکت مورد نظر را داریم:

به عنوان مثال:

import { Observable } from 'rxjs';

const observable = new Observable(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
});

const observer = observable.subscribe({
  next(x) { console.log('Received value:', x); },
  complete() { console.log('Observable completed'); }
});

observer.unsubscribe();

این کد را نمی‌توانیم با استفاده از promiseها بازنویسی کنیم. زیرا Observable سه مقدار را return می‌کند در حالی که یک promise فقط یک بار قابل resolve شدن است.

برای آزمایش بیشتر با متد unsubscribe، می‌توانیم observer دیگری اضافه کنیم که از متد takeWhile() استفاده می‌کند: اجازه می‌دهد observer منتظر بماند تا مقادیر با یک شرایط خاص مطابقت داشته باشند. به عنوان مثال، در کد زیر، eventها را از observable دریافت می‌کند در حالی که مقدار آن ۲ نیست:

import { Observable, takeWhile } from 'rxjs';

const observable = new Observable(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
});

const observer1 = observable.subscribe({
  next(x) { console.log('Received by 1 value:', x); },
  complete() { console.log('Observable 1 completed'); }
});

const observer2 = observable.pipe(
  takeWhile(value => value != "2")
).subscribe(value => console.log('Received by 2 value:', value));

در کد بالا، observer1 همان چیزی است که قبلاً دیده‌ایم: فقط subscribe می‌شود و تمام eventها را از Observable دریافت می‌کند. observer2، المنت‌هایی را از Observable دریافت می‌کند که شرط مطابقت داشته باشد. در این مورد، این بدان معنی است که value با ۲ متفاوت است.

خروجی به شکل زیر می‌باشد:

$ node observable.mjs
Received by 1 value: 1
Received by 1 value: 2
Received by 1 value: 3
Observable 1 completed
Received by 2 value: 1
$

جمع‌بندی

در این مقاله، مکانیزم جدید برای تخصیص یک promise در جاوااسکریپت را بررسی کردیم و برخی از راه‌های ممکن برای لغو یک promise قبل از تکمیل آن را یاد گرفتیم. ما همچنین promiseها را با آبجکت‌های observable مقایسه کردیم. این آبجکت‌ها نه تنها ویژگی‌های promiseها را ارائه می‌دهند، بلکه آن‌ها را با امکان انتشار چندین event و مکانیسم مناسب برای unsubscribe کردن، extend می‌کنند.