الكائن Promise في JavaScript

من موسوعة حسوب

يُمثِّل الكائن Promise إكمال (أو فشل) عملية غير متزامنة (asynchronous operation)، والنتيجة المعادة منها.

ملاحظة: هذه الصفحة تشرح الدالة البانية Promise والدوال والخاصيات التابعة لتلك الكائنات. لتعلّم المزيد عن طريقة عمل الوعود (promises) وكيف يمكنك استخدامها، فننصحك بقراءة الصفحة «استخدام الوعود» أولًا. تُستخدَم الدالة البانية بشكل رئيسي لتغليف الدوال التي لا تدعم الوعود.

إليك مثال عن استخدام الدالة البانية للكائن Promise لإنشاء وعد بسيط:

var promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('foo');
  }, 300);
});

promise1.then(function(value) {
  console.log(value);
  // "foo" الناتج المتوقع
});

console.log(promise1);
// [object Promise] الناتج المتوقع

البنية العامة

new Promise(executor /*function(resolve, reject) { ... }  */);

المعاملات

executor

الدالة التي ستُمرَّر إلى الدالة البانية والتي لها وسيطين هما resolve و reject.

الدالة executor ستُنفَّذ مباشرةً من آلية تنفيذ الوعود في المتصفح، مع تمرير الدوال resolve و reject إليها (ستُستدعى الدالة executor مباشرةً قبل أن تعيد الدالة البانية Promise كائنًا).

عند استدعاء الدالة ()resolve أو ()reject، سيُقبَل (resolve) أو يُرفَض (reject) الوعد على التوالي وبالترتيب.

ستضم الدالة executor عادةً بعض العمليات غير المتزامنة، وبعد أن تنتهي من عملها فإمَّا أن تستدعي الدالة ()resolve لقبول الوعد، أو ()reject إذا حدث خطأ.

إذا رُمي خطأٌ في الدالة executor، فسيُرفضَ الوعد، وسيتم تجاهل القيمة المُعادة من الدالة executor.

الوصف

الكائن Promise هو كائنٌ وسيط (proxy) لقيمةٍ غير معروفة تحديدًا عند إنشاء الوعد، مما يسمح لنا بربط دوال لمعالجة الأحداث غير المتزامنة التي تكون نهايتها النجاح أو الفشل. وهذا يسمح للدوال غير المتزامنة (asynchronous) أن تعيد قيمًا مثلها كمثل الدوال المتزامنة؛ لكن بدلًا من إعادة القيمة النهائية مباشرةً، فإنَّ الدوال غير المتزامنة تعيد «وعدًا» بتوفير القيمة في مرحلة ما من المستقبل.

يكون الكائن Promise في إحدى الحالات الآتية:

  • pending (قيد الانتظار): الحالة المبدئية، أي لم يُحقَّق الوعد أو يُرفَض.
  • fulfilled (محقق): يعني أنَّ العملية قد أُكمِلَت بنجاح.
  • rejected (مرفوض): يعني أنَّ العملية فشلت.
  • settled (مستقر): وهي عكس الحالة pending (قيد الانتظار) أي أن حالة الوعد قد استقرت إلى إحدى الحالتين: إمَّا fulfilled (محقق)، أو rejected (مرفوض).

الوعد الذي «قيد الانتظار» يمكن أن يكون «محققًا» ويعيد قيمةً، أو «مرفوضًا» مع سبب (خطأ). عند حدوث أحد الخيارين السابقين، فستُستدعى إحدى دوال المعالجة باستخدام الدالة then التابعة للكائن Promise.

إذا كان الوعد محققًا أو مرفوضًا عند ربط دالة المعالجة معه، فستستدعى دالة المعالجة، لذا لن تحدث «حالة سباق» (race condition) بين إكمال العملية غير المتزامنة، وبين دوال المعالجة التي يجري ربطها مع الوعد.

لمّا كانت الدالتان Promise.prototype.then()‎ و Promise.prototype.catch()‎ تعيدان بدورهما وعودًا، فيمكن استخدامهما في سلسلة.

صورة توضيحية عن سَلسَلة عدة وعود مع بعضها بعضًا.
صورة توضيحية عن سَلسَلة عدة وعود مع بعضها بعضًا.

ملاحظة: تملك بعض لغات البرمجة الأخرى آليات لتأخير تقييم قيمة تعبير أو تأجيل عملية حسابية، والتي تسمى عندها «بالوعود» (مثل Scheme). أما الوعود في JavaScript تُمثِّل العمليات التي تحدث فعلًا، والتي يمكن ربطها مع دوال رد النداء (callback functions). إذا كنتَ تبحث عن طريقة لتأخير تقييم قيمة تعبير، ففكر باستخدام الدوال السهمية دون وسائط كما في f = () => expression لتأخير تقييم قيمة التعبير، واستخدام f()‎ لتقييم قيمته.

الخاصيات

Promise.length

قيمة الخاصية length تساوي 1 دومًا، وهو عدد وسائط الدالة البانية.

Promise.prototype

تُمثِّل هذه الخاصية سلسلة prototype للدالة البانية Promise.

الدوال

Promise.all(iterable)‎

تعيد وعدًا يمكن أن يتحقق (fulfills) عندما تكون جميع الوعود في الوسيط iterable محققةً، أو يُرفَض (rejects) عند حدوث أول رفض لأحد الوعود الموجودة في الوسيط iterable.

إذا تحققت الوعود في هذه الدالة، فستتحقق هذه الدالة مع إعادة مصفوفة فيها قيم الوعود المحققة في نفس ترتيب تعريفها في الوسيط iterable. أما إذا رُفضِ أحد الوعود، فستُرفَض هذه الدالة مع سبب رفض أول وعد مُعرَّف في الوسيط iterable. يمكن الاستفادة من هذه الدالة في تجميع نتائج عدّة وعود معًا.

Promise.race(iterable)‎

تعيد وعدًا إما أن يُحقَّق أو يُرفَض عند قبول أو رفض واحد من الوعود الموجودة في iterable، مع إعادة القيمة أو السبب من ذاك الوعد.

Promise.reject(reason)‎

تعيد كائن Promise مرفوض، مع تحديد سبب (reason) الرفض.

Promise.resolve(value)‎

تعيد كائن Promise مقبول مع القيمة (value) المحددة. إذا ارتبطت هذه القيمة بالدالة ()then، فسيتّبع (follow) الوعد المعاد تلك الدالة المرتبطة بالدالة ()then، أو سيكون الوعد المُعاد محققًا مع القيمة value.

إذا لم تكن تعرف إذا كانت القيمة value وعدًا أم لا، فاستخدم الدالة Promise.resolve(value)‎ عليها، وتعامل مع القيمة المُعادة كوعد.

Promise prototype

الخاصيات

Promise.prototype.constructor

تعيد الدالة التي تُنشِئ نسخةً من الكائن Promise، وهي الدالة البانية.

الدوال

Promise.prototype.catch(onRejected)‎

تضيف دالة رد نداء (callback) للتعامل مع حالة رفض (rejection) للوعد، وتعيد وعدًا جديدًا يُقبَل (resolve) إلى القيمة المعادة من دالة رد النداء إذا اُستدعيت، أو إلى قيمة الوعد المحقق إذا تحقق الوعد بدلًا من رفضه.

Promise.prototype.then(onFulfilled, onRejected)‎

تضيف دوالًا لمعالجة حالة التحقيق والرفض لوعدٍ ما، وتعيد وعدًا جديدًا يُقبَل إلى القيمة المعادة من دالة المعالجة، أو لإلى القيمة الأصلية للوعد إذا لم يُعالَج (أي أنَّ الوسيط onFulfilled أو onRejected لم يكن دالةً).

Promise.prototype.finally(onFinally)‎

تضيف دالة معالجة إلى الوعد، وتعيد وعدًا جديدًا يقبل (resolve) عندما يُقبَل الوعد الأصلي. وستُستدعى دالة المعالجة onFinally عند انتهاء تنفيذ الوعد، سواءً تحقق أو رفض.

إنشاء وعد

يمكن إنشاء كائن Promise عن طريق الكلمة المفتاحية new والدالة البانية الخاصة به. هذه الدالة البانية تأخذ وسيطًا هو دالة تسمى «الدالة المنفذة» (executor function). ويجب أن تأخذ هذه الدالة دالتين كمعاملين، أولهما (resolve) ستستدعى عندما تكتمل المهمة غير المتزامنة بنجاح وتُعيد نتائج المهمة كقيمة، أما الدالة الثانية (reject) فستستدعى عند فشل المهمة، وتعيد سبب الفشل، ويكون عادةً كائن الخطأ.

const myFirstPromise = new Promise((resolve, reject) => {
  // القيام بعملية غير متزامنة حتى استدعاء دالة:
  //
  //   resolve(someValue);       => القبول
  // أو
  //   reject("failure reason"); => الرفض
});

لتوفير دالة لها إمكانيات الوعود، فاجعلها تعيد وعدًا:

function myAsyncFunction(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(xhr.statusText);
    xhr.send();
  });
}

أمثلة

مثال بسيط

سنستدعي الدالة resolve()‎ عند نجاح تنفيذ المهمة غير المتزامنة، والدالة reject()‎ عند فشل تنفيذ تلك المهمة.

سنستخدم في المثال الآتي الدالة setTimeout()‎ لمحاكاة شيفرة غير متزامنة. في الواقع، ستستخدم شيئًا يشبه تقنية Ajax (أي الكائن XHR) أو الواجهة البرمجية للغة HTML5.

ثم سنستخدم الدالة then()‎ لمعالجة القيمة المعادة من الوعد، وستكون قيمة المعامل successMessage مساويةً للسلسلة النصية التي مررناها للدالة resolve()‎:

let myFirstPromise = new Promise((resolve, reject) => {

  setTimeout(function(){
    resolve("Success!"); // كل شيءٍ جرى على ما يرام
  }, 250);
});

myFirstPromise.then((successMessage) => {

  console.log("Yay! " + successMessage);
});

مثال متقدم

هذا المثال الصغير يبيّن آلية عمل الكائن Promise. ستُستدعى الدالة testPromise()‎ في كل مرة يُضغط فيها على العنصر <button>، إذ سيُنشَأ وعد التي سيتحقق ويعيد عدد الوعود (العدّ يبدأ من 1) كل 1-3 ثانية (عشوائيًا) باستخدام الدالة setTimeout()‎. تُستخدَم الدالة البانية Promise()‎ لإنشاء ذاك الوعد.

سنسجِّل تحقق الوعد في كل مرة، باستخدام الدالة then()‎. وستكون هنالك سجلات تُظهِر كيف يمكن الدمج بين الشيفرة المتزامنة في الدالة مع إكمال الوعد في الشيفرة غير المتزامنة:

'use strict';
var promiseCount = 0;

function testPromise() {
    let thisPromiseCount = ++promiseCount;

    let log = document.getElementById('log');
    log.insertAdjacentHTML('beforeend', thisPromiseCount +
        ') Started (<small>Sync code started</small>)<br/>');

    // سنُنشِئ وعدًا جديدًا
    let p1 = new Promise(
        // هذه الدالة قادرة على تحقيق أو رفض الوعد
        // reject the promise
       (resolve, reject) => {
            log.insertAdjacentHTML('beforeend', thisPromiseCount +
                ') Promise started (<small>Async code started</small>)<br/>');
            // هذا مثال بسيط عن إنشاء عدم تزامن
            window.setTimeout(
                function() {
                    // حققنا الوعد
                    resolve(thisPromiseCount);
                }, Math.random() * 2000 + 1000);
        }
    );

    // then سنُعرِّف ماذا سيحدث عند قبول الوعد باستخدام 
    // catch وسنعرف ماذا سيحدث عند رفض الوعد باستخدام 
    p1.then(
        // تسجيل القيمة
        function(val) {
            log.insertAdjacentHTML('beforeend', val +
                ') Promise fulfilled (<small>Async code terminated</small>)<br/>');
        }).catch(
        // تسجيل سبب الرفض
       (reason) => {
            console.log('Handle rejected promise ('+reason+') here.');
        });

    log.insertAdjacentHTML('beforeend', thisPromiseCount +
        ') Promise made (<small>Sync code terminated</small>)<br/>');
}

دعم المتصفحات

الميزة Chrome Firefox Internet Explorer Opera Safari
الدعم الأساسي 32 29 غير مدعوم 19 8
الباني (Promise()‎) 32 29 * غير مدعوم 19 8 **
all 32 29 غير مدعوم 19 8
prototype 32 29 غير مدعوم 19 8
catch 32 29 غير مدعوم 19 8
finally 63 58 غير مدعوم 50 11.1
then 32 29 غير مدعوم 19 8
race 32 29 غير مدعوم 19 8
reject 32 29 غير مدعوم 19 8
resolve 32 29 غير مدعوم 19 8

* أصبح الباني يتطلب استعمال المعامل new معه بدءًا من الإصدار 37.

** أصبح الباني يتطلب استعمال المعامل new معه بدءًا من الإصدار 10.

المصادر