فهم التزامنات في JavaScript

مقدمة

الوعود في جافاسكريبت يمكن أن تكون صعبة الفهم. لذا أود كتابة الطريقة التي أفهم بها الوعود.

فهم الوعود

باختصار، الوعد:

“تخيل أنك طفل. أمك تعدك أنها ستشتري لك هاتف جديد الأسبوع القادم.”

أنت لا تعرف إن كنت ستحصل على ذلك الهاتف حتى الأسبوع القادم. قد تشتري لك أمك فعلاً هاتفاً جديداً، أو لا تفعل.

ذلك هو الوعد. الوعد له ثلاث حالات. وهي:

  1. معلقة: أنت لا تعرف إن كنت ستحصل على ذلك الهاتف
  2. مكتملة: الأم سعيدة، تشتري لك هاتفاً جديداً
  3. مرفوضة: الأم حزينة، لا تشتري لك هاتفاً

إنشاء وعد

لنحول هذا إلى جافاسكريبت.

// ES5: الجزء 1

var isMomHappy = false;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // تحقق
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // رفض
        }

    }
);

الكود واضح جداً بحد ذاته.

إليك كيف تبدو بنية Promise عادة:

// تبدو بنية Promise هكذا
new Promise(function (resolve, reject) { ... } );

استهلاك الPromises

الآن بعد أن حصلنا على الPromise، فلنستهلكه:

// ES5: الجزء 2

var willIGetNewPhone = ... // استمرار من الجزء 1

// استدعاء Promise الخاص بنا
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // ياي، حصلت على هاتف جديد
            console.log(fulfilled);
             // الناتج: { brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // عذراً، لم تشتريه أمي
            console.log(error.message);
             // الناتج: 'أمي ليست سعيدة'
        });
};

askMom();

فلنقم بتشغيل المثال ونرى النتيجة!

عرض: https://jsbin.com/nifocu/1/edit?js,console

سلسلة الوعود

الوعود قابلة للسلسلة.

لنفترض أنك، كطفل، توعد صديقك بأنك ستعرض عليهم الهاتف الجديد عندما تشتريك والدتك واحدًا.

هذه وعدة أخرى. لنكتبها!

// ES5

// الوعد الثاني
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

ملاحظة: يمكننا تقصير الكود أعلاه بكتابته على النحو التالي:

// تقصيره

// الوعد الثاني
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

لنسلسل الوعود. أنت، كطفل، يمكنك فقط بدء وعد showOff بعد وعد willIGetNewPhone.

// استدعاء الوعد
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // سلسلته هنا
    .then(function (fulfilled) {
            console.log(fulfilled);
         // الناتج: 'يا صديقي، لدي هاتف سامسونج أسود جديد.'
        })
        .catch(function (error) {
            // أوبس، لم تشتريه والدتي
            console.log(error.message);
         // الناتج: 'والدتي ليست سعيدة'
        });
};

هكذا يمكنك سلسلة الوعد.

الوعود غير متزامنة

الوعود غير متزامنة. لنسجل رسالة قبل وبعد استدعاء الوعد.

// استدعاء وعدنا
var askMom = function () {
    console.log('before asking Mom'); // تسجيل قبل
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('after asking mom'); // تسجيل بعد
}

ما هي التسلسل المتوقع للإخراج؟ قد تتوقع:

1. before asking Mom
2. Hey friend, I have a new black Samsung phone.
3. after asking mom

ومع ذلك، فإن التسلسل الفعلي للإخراج هو:

1. before asking Mom
2. after asking mom
3. Hey friend, I have a new black Samsung phone.

لن تتوقف عن اللعب أثناء انتظار وعد والدتك (الهاتف الجديد). هذا ما نسميه غير متزامن: سيتم تنفيذ الكود دون حظر أو انتظار النتيجة. أي شيء يحتاج إلى انتظار الوعد للمتابعة يُضاف إلى .then.

هنا مثال كامل باستخدام ES5:

// المثال الكامل ل ES5

var isMomHappy = true;

// الوعد
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // مكافئ
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // رفض
        }

    }
);

// الوعد الثاني
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

// اتصال بوعدنا
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // توصيله هنا
    .then(function (fulfilled) {
            console.log(fulfilled);
            // خريطة الخارج: 'مرحبا صديقي، لدي هاتف سامسونج أسود جديد.'
        })
        .catch(function (error) {
            // أووps، لم تشتري والدتي هذا
            console.log(error.message);
            // خريطة الخارج: 'لم تكن والدتي سعيدة'
        });
};

askMom();

الوعود في ES5، ES6/2015، ES7/Next

ES5 – أغلب المتصفحات

تعمل الشيء المعروف في بيئات ES5 (جميع المتصفحات الرئيسية + NodeJs) إذا اشتركت في Bluebird مكتبة الوعود. لأن ES5 لا يدعم الوعود بشكل مباشر. وتوجد مكتبة وعد شهيرة أخرى تدعمه Q من قبل Kris Kowal.

ES6 / ES2015 – المتصفحات الحديثة، NodeJs v6

الكود التوضيحي يعمل مباشرة لأن ES6 يدعم الوعود بشكل أصلي. بالإضافة إلى ذلك، مع وظائف ES6، يمكننا تبسيط الكود أكثر باستخدام دالة السهم واستخدام const و let.

هنا مثال كامل بكود ES6:

//_ ES6: مثال كامل_

const isMomHappy = true;

// وعد
const willIGetNewPhone = new Promise(
    (resolve, reject) => { // دالة سهم
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// الوعد الثاني
const showOff = function (phone) {
    const message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';
    return Promise.resolve(message);
};

// استدعاء وعدنا
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled)) // دالة سهم
        .catch(error => console.log(error.message)); // دالة سهم
};

askMom();

لاحظ أن جميع var تم استبدالها بـ const. كل function(resolve, reject) تم تبسيطها إلى (resolve, reject) =>. هناك العديد من الفوائد التي تأتي مع هذه التغييرات.

ES7 – Async/Await

أدخل ES7 بناء الجملة async و await. يجعل هذا البناء الجملة الغير متزامن أسهل في الفهم، دون الحاجة إلى .then و .catch.

أعد كتابة مثالنا باستخدام بناء الجملة ES7:

// ES7: مثال كامل
const isMomHappy = true;

// وعد
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// الوعد الثاني
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

// استدعاء وعدنا بأسلوب ES7 async await
async function askMom() {
    try {
        console.log('before asking Mom');

        let phone = await willIGetNewPhone;
        let message = await showOff(phone);

        console.log(message);
        console.log('after asking mom');
    }
    catch (error) {
        console.log(error.message);
    }
}

// async await هنا أيضًا
(async () => {
    await askMom();
})();

الوعود ومتى يتم استخدامها

لماذا نحتاج إلى الوعود؟ كيف كان العالم يبدو قبل الوعود؟ قبل الإجابة على هذه الأسئلة، دعونا نعود إلى الأساسيات.

الدالة العادية مقابل الدالة الغير متزامنة

دعونا نلقي نظرة على هذين المثالين. كلا المثالين يقوم بجمع رقمين: أحدهما يجمع باستخدام الدوال العادية، والآخر يجمع عن بُعد.

الدالة العادية لجمع رقمين

// جمع رقمين بشكل عادي

function add (num1, num2) {
    return num1 + num2;
}

const result = add(1, 2); // تحصل على النتيجة = 3 على الفور
الدالة الغير متزامنة لجمع رقمين
// جمع رقمين عن بُعد

// الحصول على النتيجة عن طريق استدعاء واجهة برمجة تطبيقات
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// تحصل على النتيجة = "غير معرفة"

إذا قمت بجمع الأرقام باستخدام الدالة العادية، تحصل على النتيجة على الفور. ولكن، عندما تقوم باستدعاء عن بُعد للحصول على النتيجة، تحتاج إلى الانتظار، ولا يمكنك الحصول على النتيجة على الفور.

لا تعرف إن كنت ستحصل على النتيجة لأن الخادم قد يكون متوقفًا، أو بطيئًا في الرد، إلخ. لا تريد أن يتوقف عمليتك بأكملها أثناء انتظار النتيجة.

استدعاء واجهات برمجة التطبيقات، تنزيل الملفات، وقراءة الملفات هي بعض من العمليات الغير متزامنة التي ستقوم بها.

لا تحتاج إلى استخدام الوعود لمكالمة غير متزامنة. قبل الوعود، كنا نستخدم الدوال الاستدعاء. الدوال الاستدعاء هي الدالة التي تستدعيها عندما تحصل على النتيجة المرجعة. لنعدل المثال السابق ليقبل دالة استدعاء.

// إضافة رقمين عن بُعد
// الحصول على النتيجة عن طريق استدعاء واجهة برمجة تطبيقات

function addAsync (num1, num2, callback) {
    // استخدام واجهة استدعاء getJSON الشهيرة في jQuery
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // دالة استدعاء
    const result = success; // تحصل على النتيجة = 3 هنا
});

العملية الغير متزامنة اللاحقة

بدلاً من إضافة الأرقام واحدة تلو الأخرى، نريد إضافة ثلاث مرات. في دالة عادية، سنفعل هذا:-

// إضافة رقمين بشكل عادي

let resultA, resultB, resultC;

 function add (num1, num2) {
    return num1 + num2;
}

resultA = add(1, 2); // تحصل على resultA = 3 على الفور
resultB = add(resultA, 3); // تحصل على resultB = 6 على الفور
resultC = add(resultB, 4); // تحصل على resultC = 10 على الفور

console.log('total' + resultC);
console.log(resultA, resultB, resultC);

هذا هو شكل الأمر مع الاستدعاءات الرجعية:

// إضافة رقمين عن بُعد
// الحصول على النتيجة عن طريق استدعاء واجهة برمجة تطبيقات

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // استخدام واجهة استدعاء رجعي لـ jQuery getJSON الشهيرة
	// https://api.jquery.com/jQuery.getJSON/
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // استدعاء رجعي 1
    resultA = success; // تحصل هنا على النتيجة = 3

    addAsync(resultA, 3, success => {
        // استدعاء رجعي 2
        resultB = success; // تحصل هنا على النتيجة = 6

        addAsync(resultB, 4, success => {
            // استدعاء رجعي 3
            resultC = success; // تحصل هنا على النتيجة = 10

            console.log('total' + resultC);
            console.log(resultA, resultB, resultC);
        });
    });
});

عرض توضيحي: https://jsbin.com/barimo/edit?html,js,console

هذه الصيغة أقل سهولة في الاستخدام بسبب الاستدعاءات الرجعية المتداخلة بعمق.

تجنب الاستدعاءات الرجعية المتداخلة بعمق

يمكن أن تساعدك الوعود على تجنب الاستدعاءات الرجعية المتداخلة بعمق. دعونا نلقي نظرة على نسخة الوعد من نفس المثال:

// إضافة رقمين عن بُعد باستخدام المُلاحظ القابل للمراقبة

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // استخدام واجهة برمجة تطبيقات ES6 fetch، التي تُرجع وعدًا
	// ما هو .json()؟ https://developer.mozilla.org/en-US/docs/Web/API/Body/json
    return fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json()); 
}

addAsync(1, 2)
    .then(success => {
        resultA = success;
        return resultA;
    })
    .then(success => addAsync(success, 3))
    .then(success => {
        resultB = success;
        return resultB;
    })
    .then(success => addAsync(success, 4))
    .then(success => {
        resultC = success;
        return resultC;
    })
    .then(success => {
        console.log('total: ' + success)
        console.log(resultA, resultB, resultC)
    });

مع الوعود، نُسطّح الاستدعاء مع .then. بطريقة ما، يبدو أنظف لأنه لا يوجد تداخل للاستدعاءات. مع بناء جملة ES7 async، يمكنك تحسين هذا المثال أكثر.

المُلاحظات القابلة للمراقبة

قبل أن تستقر مع الوعود، هناك شيء جديد ظهر لمساعدتك في التعامل مع البيانات الغير متزامنة تسمى المُلاحظات القابلة للمراقبة.

دعونا نلقي نظرة على نفس العرض التوضيحي مكتوب باستخدام المُلاحظات القابلة للمراقبة. في هذا المثال، سنستخدم RxJS للمُلاحظات القابلة للمراقبة.

let Observable = Rx.Observable;
let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // استخدام واجهة برمجة تطبيقات ES6 fetch، التي تُرجع وعدًا
    const promise = fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json());

    return Observable.fromPromise(promise);
}

addAsync(1,2)
  .do(x => resultA = x)
  .flatMap(x => addAsync(x, 3))
  .do(x => resultB = x)
  .flatMap(x => addAsync(x, 4))
  .do(x => resultC = x)
  .subscribe(x => {
    console.log('total: ' + x)
    console.log(resultA, resultB, resultC)
  });

يمكن للمُلاحظات القابلة للمراقبة أن تفعل أشياء أكثر إثارة للاهتمام. على سبيل المثال، delay إضافة الوظيفة بـ 3 ثوانٍ بسطر واحد فقط أو المحاولة مرة أخرى حتى تتمكن من إعادة استدعاء معين عدد من المرات.

...

addAsync(1,2)
  .delay(3000) // تأخير 3 ثوانٍ
  .do(x => resultA = x)
  ...

قد تقرأ أحد مقالاتي حول RxJs هنا.

خاتمة

إتقان الدوال الاستدعائية والوعود مهم. افهمهم واستخدمهم. لا داعي للقلق بشأن المراقبين حتى الآن. يمكن أن تلعب جميع الثلاثة دورًا في تطويرك حسب الحالة.

Source:
https://www.digitalocean.com/community/tutorials/understanding-javascript-promises