۳ سپتامبر ۲۰۲۳

مقدمه: فراخوان

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

برای نشان دادن استفاده فراخوان‌ها و پرامیس‌ها و دیگر مفاهیم انتزاعی، از برخی متد‌های مرورگر استفاده خواهیم کرد به ویژه بارگذاری اسکریپت‌ها و انجام تغییرات ساده در سند HTML.

اگر به این متدها و کاربردشان در مثال ها آشنا نیستید شاید بخواهید چند فصل از بخش بعدی آموزش را بخوانید.

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

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

به عنوان مثال، یکی از این توابع، تابع setTimeout است.

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

یک نگاه به تابع loadScript(src) بیندازید که یک اسکریپتی با src داده شده را بارگذاری می‌کند.

‍‍‍

function loadScript(src) {
    //یک تگ اسکریپت می‌سازد و آن را به صفحه اضافه می‌کند
    // داده شده شروع به بارگذاری کند و زمانی که بارگذاری کامل شد اجرا شود src این باعث می‌شود اسکریپت با
    let script = document.createElement("script");
    script.src = src;
    document.head.append(script);
}

این تابع یک تگ <script src="...">‎ را به صورت پویا ایجاد می‌کند و با src داده شده به سند اضافه می‌کند. مرورگر به طور خودکار شروع به بارگذاری آن می‌کند و زمانی که کامل شد، اجرایش می‌کند.

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

// اسکریپت با مسیر داده شده بارگذاری و اجرا می‌کند
loadScript("/my/script.js");

اسکریپت به صورت غیرهمگام اجرا می‌شود، زیرا الآن شروع به بارگذاری می‌کند، اما بعدا (زمانی‌که کار تابع تمام شده)، اجرا می‌شود.

اگر کدی بعد از ‍‍loadScript(...)‎ وجود داشته باشد، منتظر بارگذاری کامل اسکریپت نمی‌ماند.

loadScript("/my/script");
// loadScript کدهای پس از تابع
// منتظر بارگذاری کامل اسکریپت نمی‌ماند
// ...

فرض کنید نیاز داریم بلافاصله بعد از بارگذاری اسکریپت جدید از آن استفاده کنیم. آن اسکریپت توابع جدیدی اعلام می‌کند و ما می‌خواهیم آن‌ها را فراخوانی کنیم.

اما اگر این کار را فورا بعد از صدا زدن ‍‍loadScript(...)‎ انجام دهیم کار نخواهد کرد.

loadScript("/my/script.js"); // است function newFunction() {...} شامل

newFunction();

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

بیایید به عنوان آرگومان دوم loadScript یک تابع callback اضافه کنیم که زمانی که اسکریپت بارگذاری شد، اجرا شود:

function leadScript(src, callback) {
    let script = document.createElement("script");
    script.src = src;

    script.onload = () => callback(script);

    document.head.append(script);
}

رویداد onload در مقاله Resource loading: onload and onerror توضیح داده شده است؛ درواقع این رویداد تابعی را بعد از اینکه اسکریپت بارگیری و اجرا شد، اجرا می‌کند.

حالا اگر بخواهیم تابع‌های جدیدی از اسکریپت را فراخوانی کنیم، باید آن را در callback بنویسیم:

loadScript('/my/script.js', function() {
  // بعد از اینکه اسکریپت بارگیری شد، اجرا می‌شود callback این
  newFunction(); // حالا کار می‌کند
  ...
});

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

این‌هم یک مثال قابل اجرا با یک اسکریپت واقعی:

function loadScript(src, callback) {
    let script = document.createElement("script");
    script.src = src;
    script.onload = () => callback(script);
    document.head.append(script);
}

leadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js", () => {
    alert(`اسکریپت ${script.src} بارگیری شد`);
    alert( _ ); // یک تابع تعریف شده در اسکریپت بارگیری شده است _
});

این “سبک برنامه‌نویسی غیرهمگام مبتنی بر callback” نامیده می‌شود. یک تابع که کاری را به صورت غیرهمگام انجام می‌دهد، باید آرگومان callback دریافت کند که تابعی را که بعد از اتمام کار اجرا شود، در آن قرار دهیم.

در اینجا ما این عمل را در loadScript انجام دادیم، اما البته این یک رویکرد کلی است.

فراخوان در فراخوان

چگونه می‌توانیم دو اسکریپت را به ترتیب بارگذاری کنیم: اول اسکریپت اول و بعد از آن اسکریپت دوم؟

راه حل طبیعی، قرار دادن تابع دوم loadScript درون callback اول است، مانند این:

loadScript("/my/script.js" , function(script) {

    alert(`عالی، ${script.src} بارگذاری شد، حالا یکی دیگر را بارگیری می‌کنیم`);

    loadScript("/my/script2.js", function(script) {
        alert(`عالی، اسکریپت دوم بارگذاری شد`);
    });

});

وقتی تابع بیرونی loadScript تمام شد، callback آن تابع درونی را شروع می‌کند.

اگر یک اسکریپت دیگر هم بخواهیم چطور؟

loadScript("/my/script.js", function(script) {

    loadScript("/my/script2.js", function(script) {

        loadScript("/my/script3.js", function(script) {
            // ...ادامه کار بعد از بارگذاری همه اسکریپت‌ها
        });

    });

});

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

مدیریت خطاها

در مثال های بالا هیچ خطایی را در نظر نگرفته بودیم. اگر بارگیری با مشکل مواجه شود چه می‌شود؟ callback های ما باید بتوانند نسبت به آن واکنش نشان بدهند.

این نسخه بهبود یافته loadScript است که خطاهای بارگذاری را ردیابی می‌کند:

function loadScript(src, callback) {
    let script = document.createElement("script");
    script.src = src;

    script.onload = () => callback(null, script);
    script.onerror = () => callback(new Error(` خطا در بارگذاری اسکریپت برای ${src}`));

    document.head.append(script);
}

در صورت بارگذاری موفق callback(null, script) و در غیر این صورت callback(error) صدا زده می‌شود.

به این صورت استفاده می‌شود:

loadScript("/my/script.js", function(error, script) {
    if (error) {
        // مدیریت خطاها
    }else {
        // اسکریپت با موفقیت بارگیری شده است
    }
});

یک بار دیگر، الگویی که برای loadScript استفاده کردیم، در واقع کاملاً رایج است. به آن “error-first callback” می‌گویند.

قرارداد این است:

  1. آرگومان اول callback برای خطا در صورت بروز اختصاص داده شده است. سپس callback(err) صدا زده می‌شود.
  2. آرگومان دوم (و آرگومان‌های بعدی اگر لازم باشد) برای نتیجه موفقیت‌آمیز هستند. سپس callback(null, result1, result2...) صدا زده می‌شود.

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

هرم مصیبت‌بار

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

اما برای چندین عمل غیرهمگام که پشت سر هم اجرا می‌شوند، کدی شبیه این خواهیم داشت:

loadScript("1.js", function(error, script) {

    if(error) {
        handleError(error);
    }else {
        //...
        loadScript("2.js", function(error, script) {
            if(error) {
                handleError(error);
            }else {
                //...
                loadScript("3.js", function(error, script) {
                    if(error) {
                        handleError(error);
                    }else {
                        //... تا زمانی که همه اسکریپت ها بارگیری شوند ادامه داد
                    }
                });

            }
        });
    }
});

در کد بالا داریم:

  1. 1.js را بارگیری می‌کنیم سپس اگر خطایی نبود
  2. 2.js را بارگیری می‌کنیم سپس اگر خطایی نبود
  3. 3.js را بارگیری می‌کنیم سپس اگر خطایی نبود – کار دیگری انجام می‌دهیم(*)

هرچه تودرتویی‌ها بیشتر شود، کد عمیق‌تر می‌شود و مدیریت آن سخت‌تر می‌شود، به ویژه اگر به جای ... کد واقعی داشته باشیم که شامل حلقه‌ها، شرطی‌ها و … بیشتری باشد.

گاهی اوقات به این “جهنم callback (callback hell)” یا “هرم مصیبت‌بار (pyramid of doom)” می‌گویند.

“هرم” تودرتویی‌ها با هر عمل غیرهمگام جدید به سمت راست رشد می‌کند. به زودی کنترل آن از دست می‌رود.

پس این روش کدنویسی خیلی خوب نیست.

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

loadScript("1.js", step1);

function step1(error, script) {
    if(error) {
        handleError(error);
    }else {
        //...
        loadScript("2.js", step2);
    }
}

function step3(error, script) {
    if(error) {
        handleError(error);
    }else {
        //...
        loadScript("3.js", step3);
    }
}

function step3(error, script) {
    if(error) {
        handleError(error);
    }else {
        //... تا زمانی که همه اسکریپت ها بارگیری شوند ادامه داد
    }
}

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

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

همچنین توابعِ step*‎ فقط یکبار استفاده می‌شوند و فقط برای اجتناب از “هرم مصیبت‌بار” ایجاد شده‌اند. که باعث بی نظمی زیادی در کد می‌شوند.

ما می‌خواهیم چیز بهتری داشته باشیم.

خوشبختانه راه‌های دیگری برای اجتناب از این هرم‌ها وجود دارد. یکی از بهترین راه‌ها استفاده از “promise” است که در فصل بعد توضیح داده می‌شود.

نقشه آموزش