الفرق بين المراجعتين ل"JavaScript/Promise/Using promises"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
(إنشاء الصفحة)
 
(إضافة التنسيق)
سطر 1: سطر 1:
# استخدام الكائن Promise
+
<noinclude>{{DISPLAYTITLE:استخدام الكائن <code>Promise</code> في JavaScript}}</noinclude>
 
 
يُعرّف الكائن [Promise](https://wiki.hsoub.com/JavaScript/Promise) أو «الوعد» على أنّه كائنُ يمثّلُ النّتيجة النّهائيّة من إكمالٍ أو فشلٍ لعمليّةٍ غير متزامنة. ولمّا كان أغلب المُطورين يستخدمون الوعود الجاهزة (المنشأة مسبقًا)، فسيشرح هذا الدّليل استخدام الوعود المُعادة قبل التّطرّق إلى كيفيّة إنشائها.
 
  
 +
يُعرّف الكائن [[JavaScript/Promise|<code>Promise</code>]] أو «الوعد» على أنّه كائنُ يمثّلُ النّتيجة النّهائيّة من إكمالٍ أو فشلٍ لعمليّةٍ غير متزامنة. ولمّا كان أغلب المُطورين يستخدمون الوعود الجاهزة (المنشأة مسبقًا)، فسيشرح هذا الدّليل استخدام الوعود المُعادة قبل التّطرّق إلى كيفيّة إنشائها.
  
 
يمكننا القول أنّ «الوعد» (promise) في جوهره ما هو إلّا كائنٌ مُعادٌ تقوم بإرفاق ردود النّداء (Callbacks) معه عوضًا عن تمريرها إلى الدّالّة.
 
يمكننا القول أنّ «الوعد» (promise) في جوهره ما هو إلّا كائنٌ مُعادٌ تقوم بإرفاق ردود النّداء (Callbacks) معه عوضًا عن تمريرها إلى الدّالّة.
  
تخيّل أنّ لدينا الدّالّة `createAudioFileAsync()‎` والتي تُولّد بطريقةٍ غير متزامنةٍ ملفًّا صوتيًّا انطلاقًا من ضبط الملف الصوتي ودالّتي رد نداء تُستدعَى الأولى عند نجاح إنشاء الملف الصوتي والثّانية عند حدوث أخطاء. إليك شيفرة تستخدم الدّالّة `createAudioFileAsync()‎`:
+
تخيّل أنّ لدينا الدّالّة <code>createAudioFileAsync()‎</code> والتي تُولّد بطريقةٍ غير متزامنةٍ ملفًّا صوتيًّا انطلاقًا من ضبط الملف الصوتي ودالّتي رد نداء تُستدعَى الأولى عند نجاح إنشاء الملف الصوتي والثّانية عند حدوث أخطاء. إليك شيفرة تستخدم الدّالّة <code>createAudioFileAsync()‎</code>:<syntaxhighlight lang="javascript">
 
 
```js
 
 
function successCallback(result) {
 
function successCallback(result) {
 
   console.log("Audio file ready at URL: " + result);
 
   console.log("Audio file ready at URL: " + result);
سطر 18: سطر 15:
  
 
createAudioFileAsync(audioSettings, successCallback, failureCallback);
 
createAudioFileAsync(audioSettings, successCallback, failureCallback);
```
 
 
  
تُعيد الدّوالُّ الحديثة وعدًا يمكنك إرفاق ردود النّداء معه عوضًا عن ذلك.
+
</syntaxhighlight>تُعيد الدّوالُّ الحديثة وعدًا يمكنك إرفاق ردود النّداء معه عوضًا عن ذلك.
  
إذا ما أُعيدت كتابة الدّالّة `createAudioFileAsync()‎لتُعيدَ وعدًا، يصبح استخدامها بهذه البساطة:
+
إذا ما أُعيدت كتابة الدّالّة <code>createAudioFileAsync()‎</code> لتُعيدَ وعدًا، يصبح استخدامها بهذه البساطة:<syntaxhighlight lang="javascript">
 
 
```js
 
 
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
 
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
```
 
  
يُمثّل ما سبق اختزالًا لما يلي:
+
</syntaxhighlight>يُمثّل ما سبق اختزالًا لما يلي:<syntaxhighlight lang="javascript">
 
 
```js
 
 
const promise = createAudioFileAsync(audioSettings);  
 
const promise = createAudioFileAsync(audioSettings);  
 
promise.then(successCallback, failureCallback);
 
promise.then(successCallback, failureCallback);
```
 
  
نُطلق على هذه الحالة استدعاء دالّة استدعاءً غير متزامن. يمنحنا هذا الاصطلاح عددًا من الميّزات سنتطّرق لكل منها على حدة.
+
</syntaxhighlight>نُطلق على هذه الحالة استدعاء دالّة استدعاءً غير متزامن. يمنحنا هذا الاصطلاح عددًا من الميّزات سنتطّرق لكل منها على حدة.
  
## الضمانات
+
== الضمانات ==
على نقيض ردود النّداء المُمَرّرة ذات النمط القديم، يأتي `Promise` مع بعض الضمانات:
+
على نقيض ردود النّداء المُمَرّرة ذات النمط القديم، يأتي [[JavaScript/Promise|<code>Promise</code>]] مع بعض الضمانات:
+
* لن تُطلَب ردود النّداء قبل [https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#Run-to-completion إتمام التنفيذ الحالي] لحلقة الأحداث في JavaScript.
- لن تُطلَب ردود النّداء قبل [إتمام التنفيذ الحالي](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#Run-to-completionلحلقة الأحداث في JavaScript.
+
* تًطلب ردود النداء المضافة باستخدام <code>[[JavaScript/Promise/then|then()‎]]</code> حتى بعد نجاح أو فشل العملية غير المتزامنة وفق ما ذكر أعلاه.
تًطلب ردود النداء المضافة باستخدام [`then()`](https://wiki.hsoub.com/JavaScript/Promise/then) حتى بعد نجاح أو فشل العملية غيرالمتزامنة وفق ما ذكر أعلاه.
+
* يُمكن إضافة عدة ردود نداء من خلال استدعاء [[JavaScript/Promise/then|<code>then()‎</code>]] عدة مرات. تُنفّذ جميع ردود النداء واحدًا تلو الآخر بنفس الترتيب الذي أدرجت به.
- يُمكن إضافة عدة ردود نداء من خلال استدعاء [`then()`](https://wiki.hsoub.com/JavaScript/Promise/then) عدة مرات. تُنفّذ جميع ردود النداء واحدًا تلو الآخر بنفس الترتيب الذي أدرجت به.
+
يُقدم استخدام الوعود العديد من الميزات الرائعة، نذكرالآن منها '''استخدام السّلسَلة (chaining)'''.
  
يُقدم استخدام الوعود العديد من الميزات الرائعة، نذكرالآن منها **استخدام السّلسَلة chaining.**
+
== السّلسََلة (الاستدعاء المتسلسل) ==
 +
يُعدُّ التنفيذ التّعاقبيُّ لعمليّتَين غير متزامنتَين أو أكثر حاجةً مشتركةً لدى العديد من المُطوّرين، حين تقتضي الضّرورة أن تبدأ العمليّة اللّاحقة فور نجاح سابقتها ومعتمدةً على النّتيجة من تلك الأخيرة. نُنجز هذا من خلال إنشاء سلسلة من الوعود ('''promise chain''').  
  
 
+
هنا يكمن الإبداع: تٌعيد الدّالّة <code>then()‎</code> كائن [[JavaScript/Promise|'''<code>Promise</code>''']] جديد مختلفًا عن الأصليّ''':'''<syntaxhighlight lang="javascript">
## السّلسََلة (الاستدعاء المتسلسل)
 
 
 
يُعدُّ التنفيذ التّعاقبيُّ لعمليّتَين غير متزامنتَين أو أكثر حاجةً مشتركةً لدى العديد من المُطوّرين، حين تقتضي الضّرورة أن تبدأ العمليّة اللّاحقة فور نجاح سابقتها ومعتمدةً على النّتيجة من تلك الأخيرة. نُنجز هذا من خلال إنشاء سلسلة من الوعود **promise chain**.
 
 
 
هنا يكمن الإبداع: تٌعيد الدّالّة `then()‎` كائن **`Promise` جديد مختلفًا عن الأصليّ:**
 
 
 
```js
 
 
const promise = doSomething();
 
const promise = doSomething();
 
const promise2 = promise.then(successCallback, failureCallback);
 
const promise2 = promise.then(successCallback, failureCallback);
```
 
  
أو:
+
</syntaxhighlight>أو:<syntaxhighlight lang="javascript">
```js
 
 
const promise2 = doSomething().then(successCallback, failureCallback);
 
const promise2 = doSomething().then(successCallback, failureCallback);
```
 
 
لا يُمثّل الكائن (`promise2`) إتمام `doSomething()‎` فحسب، إنّما يشمل ما قمت بتمريره سواء `successCallback` أو `failureCallback`؛ الأمر الذي قد يُمثّل دوالًا غير متزامنة أخرى تقوم بدورها بإرجاع وعد. في هذه الحالة، تُوضع أيّة ردود نداء مضافة إلى `promise2` في طابور وذلك خلف الوعد المُعاد إمّا من قبل `successCallback` أو `failureCallback`.
 
  
 +
</syntaxhighlight>لا يُمثّل الكائن <code>promise2</code> إتمام <code>doSomething()‎</code> فحسب، إنّما يشمل ما قمت بتمريره سواء <code>successCallback</code> أو <code>failureCallback</code>؛ الأمر الذي قد يُمثّل دوالًا غير متزامنة أخرى تقوم بدورها بإرجاع وعد. في هذه الحالة، تُوضع أيّة ردود نداء مضافة إلى <code>promise2</code> في طابور وذلك خلف الوعد المُعاد إمّا من قبل <code>successCallback</code> أو <code>failureCallback</code>.
  
 
يُمثّل كل وعدٍ بصفةِ أساسيّةٍ إتمام خطوة غير متزامنة أخرى في السّلسلة.
 
يُمثّل كل وعدٍ بصفةِ أساسيّةٍ إتمام خطوة غير متزامنة أخرى في السّلسلة.
  
أدى تنفيذ عدّة عمليّات غير متزامنة على التّسلسل في السّابق إلى حالة هرم الموت (pyramid of doom):
+
أدى تنفيذ عدّة عمليّات غير متزامنة على التّسلسل في السّابق إلى حالة هرم الموت (pyramid of doom):<syntaxhighlight lang="javascript">
 
 
```js
 
 
doSomething(function(result) {
 
doSomething(function(result) {
 
   doSomethingElse(result, function(newResult) {
 
   doSomethingElse(result, function(newResult) {
سطر 79: سطر 56:
 
   }, failureCallback);
 
   }, failureCallback);
 
}, failureCallback);
 
}, failureCallback);
```
 
  
نُرفق كحلّ بديل في الدّوال الحديثة ردود النداء مع الوعود المُعادة  مما يُشكّل سلسلة وعود:
+
</syntaxhighlight>نُرفق كحلّ بديل في الدّوال الحديثة ردود النداء مع الوعود المُعادة  مما يُشكّل سلسلة وعود:<syntaxhighlight lang="javascript">
 
 
```js
 
 
doSomething()
 
doSomething()
 
.then(function(result) {
 
.then(function(result) {
سطر 95: سطر 69:
 
})
 
})
 
.catch(failureCallback);
 
.catch(failureCallback);
```
 
  
إضافة المُتغيّرات إلى `then` أمرٌ غير إلزاميّ، أمّا `catch(failureCallback)‎` فهي اختزال للدّالّة `then(null, failureCallback)‎` وقد تُصادِف في المقابل هذا الأمر مُمثّلًا [بدوال سهميّة](https://wiki.hsoub.com/JavaScript/Arrow_Functions).
+
</syntaxhighlight>إضافة المُتغيّرات إلى <code>then</code> أمرٌ غير إلزاميّ، أمّا <code>catch(failureCallback)‎</code> فهي اختزال للدّالّة <code>then(null, failureCallback)‎</code> وقد تُصادِف في المقابل هذا الأمر مُمثّلًا [[JavaScript/Arrow Functions|بدوال سهميّة]].<syntaxhighlight lang="javascript">
 
 
```js
 
 
doSomething()
 
doSomething()
 
.then(result => doSomethingElse(result))
 
.then(result => doSomethingElse(result))
سطر 107: سطر 78:
 
})
 
})
 
.catch(failureCallback);
 
.catch(failureCallback);
```
 
  
 +
</syntaxhighlight>'''ملاحظة مهمة''': قُم دائمًا بإرجاع نتائج وإلّا لن تقوم ردود النداء بالتقاط نتيجة الوعد السّابق (تُمثّل في الدّوال السّهميّة <code>‎() => x</code>  اختزالًا للتّركيب <code>‎() => { return x; }‎</code>).
  
**ملاحظة مهمة**: قُم دائمًا بإرجاع نتائج وإلّا لن تقوم ردود النداء بالتقاط نتيجة الوعد السّابق (تُمثّل في الدّوال السّهميّة `‎() => x`  اختزالًا للتّركيب `‎() => { return x; }‎`).
+
=== استخدام السّلاسل بعد <code>catch</code> ===
 
+
يُمكن استخدام السّلاسل بعد الفشل في حالة <code>catch</code>، إذ يفيد هذا في إنجاز إجراءات جديدة حتى بعد فشل إجراء في سلسلة الاستدعاء. إليك المثال التّالي:<syntaxhighlight lang="javascript">
### استخدام السّلاسل بعد `catch`
 
 
 
يُمكن استخدام السّلاسل بعد الفشل في حالة `catch`، إذ يفيد هذا في إنجاز إجراءات جديدة حتى بعد فشل إجراء في سلسلة الاستدعاء. إليك المثال التّالي:
 
 
 
```js
 
 
new Promise((resolve, reject) => {
 
new Promise((resolve, reject) => {
 
     console.log('Initial');
 
     console.log('Initial');
سطر 133: سطر 99:
 
     console.log('Do this, no matter what happened before');
 
     console.log('Do this, no matter what happened before');
 
});
 
});
```
 
 
تُخرج الشّيفرة السّابقة النّص التّالي:
 
  
```html
+
</syntaxhighlight>تُخرج الشّيفرة السّابقة النّص التّالي:<syntaxhighlight lang="javascript">
 
Initial
 
Initial
 
Do that
 
Do that
 
Do this, no matter what happened before
 
Do this, no matter what happened before
```
 
  
 +
</syntaxhighlight>'''ملاحظة''': لا يُعرض النص "Do this" لأن الخطاً "Something failed" تسبّب بحدوث رفض.
  
 
+
== انتشار الخطأ ==
**ملاحظة**: لا يُعرض النص "Do this" لأن الخطاً "Something failed" تسبّب بحدوث رفض.
+
قد تعود إلى ذهنك رؤية الدّالّة <code>failureCallback</code> ثلاث مرّاتٍ في هرم الموت أعلاه بالمقارنة مع ورودها مّرةً واحدة في نهاية سلسلة وعود:<syntaxhighlight lang="javascript">
## انتشار الخطأ
 
قد تعود إلى ذهنك رؤية الدّالّة `failureCallback` ثلاث مرّاتٍ في هرم الموت أعلاه بالمقارنة مع ورودها مّرةً واحدة في نهاية سلسلة وعود:
 
```js
 
 
doSomething()
 
doSomething()
 
.then(result => doSomethingElse(result))
 
.then(result => doSomethingElse(result))
سطر 154: سطر 114:
 
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
 
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
 
.catch(failureCallback);
 
.catch(failureCallback);
```
 
 
تتوقّف سلسلة الوعود عند وجود استثناء لتبحث عوض ذلك أدناه عن معالجات catch. نُمذج هذا الأمر على نحوٍ مماثل للغاية لعمل الشّيفرة البرمجيّة المتزامنة.
 
  
```js
+
</syntaxhighlight>تتوقّف سلسلة الوعود عند وجود استثناء لتبحث عوض ذلك أدناه عن معالجات catch. نُمذج هذا الأمر على نحوٍ مماثل للغاية لعمل الشّيفرة البرمجيّة المتزامنة.<syntaxhighlight lang="javascript">
 
try {
 
try {
 
   const result = syncDoSomething();
 
   const result = syncDoSomething();
سطر 167: سطر 124:
 
   failureCallback(error);
 
   failureCallback(error);
 
}
 
}
```
 
 
يبلغُ هذا التماثل ذروته عند استخدام التجميل اللّغوي [`async`/`await`](https://wiki.hsoub.com/JavaScript/Async_Function_Expression&sa=D&ust=1558696425360000&usg=AFQjCNHR4pppLwi1ln2CbckVk_YLT92-Uw) في ECMAScript 2017:
 
  
```js
+
</syntaxhighlight>يبلغُ هذا التماثل ذروته عند استخدام التجميل اللّغوي [[JavaScript/Async Function Expression|<code>async</code>/<code>await</code>]] في ECMAScript 2017:<syntaxhighlight lang="javascript">
 
async function foo() {
 
async function foo() {
 
   try {
 
   try {
سطر 182: سطر 136:
 
   }
 
   }
 
}
 
}
```
 
 
يعتمد الأمر تمامًا على الوعود، على سبيل المثال `doSomething()‎` هي نفسها الدّالّة السّابقة. إذا أردت، يمكنك قراءة [المزيد](https://developers.google.com/web/fundamentals/getting-started/primers/async-functions) حول هذه الصّياغة.
 
 
تُقدّم الوعود حلًّا لخللٍ جوهريٍّ في استدعاء هرم الموت، ذلك بالتقاط catch جميعَ الأخطاء حتى الاستثناءات المرميّة منها وأخطاء البرمجة. هذا الأمر في غاية الأهميّة للتركيب الوظيفيّ للعمليات غير المتزامنة.
 
 
## أحداث رفض الوعود
 
  
 +
</syntaxhighlight>يعتمد الأمر تمامًا على الوعود، على سبيل المثال <code>doSomething()‎</code> هي نفسها الدّالّة السّابقة. إذا أردت، يمكنك قراءة المزيد حول هذه الصّياغة.
  
عند رفض وعدٍ، يُرسَل أحد حدثين اثنين إلى النّطاق العمومي (يكون هذا النّطاق غالباً إما النّافذة [`window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) أو عند الاستخدام في عامل وِيب، العامل [`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker) أو واجهة أخرى مبنيّة على عامل). الحدثان المذكوران أعلاه هما:
+
تُقدّم الوعود حلًّا لخللٍ جوهريٍّ في استدعاء هرم الموت، ذلك بالتقاط (catch) جميعَ الأخطاء حتى الاستثناءات المرميّة منها وأخطاء البرمجة. هذا الأمر في غاية الأهميّة للتركيب الوظيفيّ للعمليات غير المتزامنة.
  
[`rejectionhandled`](https://developer.mozilla.org/en-US/docs/Web/API/Window/rejectionhandled_even)
+
== أحداث رفض الوعود ==
 +
عند رفض وعدٍ، يُرسَل أحد حدثين اثنين إلى النّطاق العمومي (يكون هذا النّطاق غالباً إما النّافذة <code>window</code> أو عند الاستخدام في عامل وِيب، العامل <code>Worker</code> أو واجهة أخرى مبنيّة على عامل). الحدثان المذكوران أعلاه هما:
  
يُرسل عندما يُرفض الوعد بعد أن يعالج الخطأ من قبل دالّة `reject` الخاصّة بالمنفّذ.
+
=== <code>rejectionhandled</code> ===
 
+
يُرسل عندما يُرفض الوعد بعد أن يعالج الخطأ من قبل دالّة <code>reject</code> الخاصّة بالمنفّذ.
[`unhandledrejection`](https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event )
 
  
 +
=== <code>unhandledrejection</code> ===
 
يُرسل عندما يُرفض الوعد ولا يتوفّر هناك معالج رفض.
 
يُرسل عندما يُرفض الوعد ولا يتوفّر هناك معالج رفض.
  
في كلتي الحالتين، يتوفّر الحدث من النّوع [`PromiseRejectionEvent`](https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent ") على [`promise`](https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent/promise) كعضوٍ للإشارة إلى الوعد المرفوض  وعلى واصفة [`reason`](https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent/reason) لتبيان السّبب وراء رفض هذا الوعد.
+
في كلتي الحالتين، يتوفّر الحدث من النّوع <code>PromiseRejectionEvent</code> على <code>promise</code> كعضوٍ للإشارة إلى الوعد المرفوض  وعلى واصفة <code>reason</code> لتبيان السّبب وراء رفض هذا الوعد.
  
 
يسمح ما سبق بتقديم معالجة أخطاء للوعود في حالات التّراجع، كما يساعد في تنقيح مشاكل إدارة الوعود لديك. تكون هذه المُعالجات عموميّة على السّياق، لذا تذهب جميع الأخطاء إلى معالجات الأحداث نفسها بغضّ النّظر عن مصدرها.
 
يسمح ما سبق بتقديم معالجة أخطاء للوعود في حالات التّراجع، كما يساعد في تنقيح مشاكل إدارة الوعود لديك. تكون هذه المُعالجات عموميّة على السّياق، لذا تذهب جميع الأخطاء إلى معالجات الأحداث نفسها بغضّ النّظر عن مصدرها.
  
للوقوف على إحدى الفوائد الهامّة لما ذكر أعلاه نعرض التّالي: من الشّائع عند كتابة شيفرة برمجية لبيئة [Node.js](https://developer.mozilla.org/en-US/docs/Glossary/Node.js ) أن تكون الوحدات التي تُضمّنها في مشروعك ذات وعودٍ مرفوضة وغير مدَاوَلة. تُسجّل هذه الوعود في الكونسول من قبل Node runtime. يمكنك التقاطها في شيفرتك البرمجيّة للتّحليل والمعالجة أو لتفادي بعثرتها في خرج شيفرتك وذلك ومن خلال إضافة معالج للحدث [`unhandledrejection`](https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event) كما يلي:
+
للوقوف على إحدى الفوائد الهامّة لما ذكر أعلاه نعرض التّالي: من الشّائع عند كتابة شيفرة برمجية لبيئة Node.js أن تكون الوحدات التي تُضمّنها في مشروعك ذات وعودٍ مرفوضة وغير مدَاوَلة. تُسجّل هذه الوعود في الكونسول من قبل مُشغِّل Node الآني (Node runtime). يمكنك التقاطها في شيفرتك البرمجيّة للتّحليل والمعالجة أو لتفادي بعثرتها في خرج شيفرتك وذلك ومن خلال إضافة معالج للحدث <code>unhandledrejection</code> كما يلي:<syntaxhighlight lang="javascript">
 
 
```js
 
 
window.addEventListener("unhandledrejection", event => {
 
window.addEventListener("unhandledrejection", event => {
 
  /* يُمكنك أن تبدأ هنا بإضافة الشّيفرة البرمجيّة
 
  /* يُمكنك أن تبدأ هنا بإضافة الشّيفرة البرمجيّة
سطر 215: سطر 162:
 
   event.preventDefault();
 
   event.preventDefault();
 
}, false);
 
}, false);
```
 
 
 
باستدعاء تابع [`preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) الخاصّة بالحدث، تقوم بتوجيه مُشغل JavaScript الآني (JavaScript runtime) إلى عدم اتّخاذ إجراءه الافتراضي عندما لا تُعالج الوعود المرفوضة. عادةً ما يتضمن الإجراء الافتراضي هذا تسجيل الخطأ في الطرفية وهذه هي الحالة فعلًا في Node.
 
  
 +
</syntaxhighlight>باستدعاء تابع <code>preventDefault()‎</code> الخاصّة بالحدث، تقوم بتوجيه مُشغل JavaScript الآني (JavaScript runtime) إلى عدم اتّخاذ إجراءه الافتراضي عندما لا تُعالج الوعود المرفوضة. عادةً ما يتضمن الإجراء الافتراضي هذا تسجيل الخطأ في الطرفية وهذه هي الحالة فعلًا في Node.
  
 
يتوجّب عليك في الحالة المثلى فحص الوعود المرفوضة قبل نبذ الأحداث هذه حرصًا أن يكون أيّ منها علّة فعليّة في الشّيفرة البرمجيّة.
 
يتوجّب عليك في الحالة المثلى فحص الوعود المرفوضة قبل نبذ الأحداث هذه حرصًا أن يكون أيّ منها علّة فعليّة في الشّيفرة البرمجيّة.
  
## إنشاء الوعد حول واجهة برمجة تطبيقات API ذات ردِّ نداء قديم
+
== إنشاء الوعد حول واجهة برمجة تطبيقات API ذات ردِّ نداء قديم ==
 
 
 
يمكن إنشاء وعدٍ من الصّفر باستخدام بانيه. نحتاج هذا الأمر عند تغليف واجهات برمجة تطبيقات قديمة.  
 
يمكن إنشاء وعدٍ من الصّفر باستخدام بانيه. نحتاج هذا الأمر عند تغليف واجهات برمجة تطبيقات قديمة.  
  
تُعيد جميع الدّوال غير المتزامنة وعودًا في الحالة المثلى. لكن لسوء الحظّ، لا تزال بعض واجهات برمجة التطبيقات تنتظر تمرير نتيجة الاستدعاء من نجاح و/أو فشل وفق الطّريقة المتّبعة قديمًا. يتجلّى هذا بأوضح صوره في الدّالة [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout).
+
تُعيد جميع الدّوال غير المتزامنة وعودًا في الحالة المثلى. لكن لسوء الحظّ، لا تزال بعض واجهات برمجة التطبيقات تنتظر تمرير نتيجة الاستدعاء من نجاح و/أو فشل وفق الطّريقة المتّبعة قديمًا. يتجلّى هذا بأوضح صوره في الدّالة <code>setTimeout()‎</code>.<syntaxhighlight lang="javascript">
 
 
```js
 
 
setTimeout(() => saySomething("10 seconds passed"), 10000);
 
setTimeout(() => saySomething("10 seconds passed"), 10000);
```
 
  
يتسبب المزج بين ردود النداء ذات النمط القديم والوعود بالكثير من المشاكل.  
+
</syntaxhighlight>يتسبب المزج بين ردود النداء ذات النمط القديم والوعود بالكثير من المشاكل. لا يوجد ما يلتقط الخطأ إذا ما فشل <code>saySomething()‎</code> أو احتوى على خطأ برمجي وتقع المسؤولية في هذا على عاتق <code>setTimeout</code> .
لا يوجد ما يلتقط الخطأ إذا ما فشل `saySomething()‎` أو احتوى على خطأ برمجي وتقع المسؤولية في هذا على عاتق `setTimeout` .
 
  
لحسن الحظ، يمكننا تغليف `setTimeout` ضمن الوعد. الممارسة المثلى في هذه الحالة هي تغليف الدوال الإشكالية في أدنى مستوى ممكن ومن ثم عدم استدعائها مباشرة بعد ذلك:
+
لحسن الحظ، يمكننا تغليف <code>setTimeout</code> ضمن الوعد. الممارسة المثلى في هذه الحالة هي تغليف الدوال الإشكالية في أدنى مستوى ممكن ومن ثم عدم استدعائها مباشرة بعد ذلك:<syntaxhighlight lang="javascript">
 
 
```js
 
 
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
 
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
  
 
wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);
 
wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);
```
 
 
يأخذ باني الوعد دالّة تنفيذية تسمح لنا بحل أو رفض كائن الوعد يدويًّا. لما كان فشل الدالة `setTimeout()‎` مستبعدًا، فقد أهملنا وضع رفضٍ في هذه الحالة.
 
 
## التّركيب
 
  
يُعدّ [`Promise.resolve()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) و [`Promise.reject()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject ) اختصارين لإنشاء وعدٍ محلولٍ بالفعل أو مرفوضٍ بالفعل على التّرتيب يدويًا. نجد هذا مفيدًا جدًا في بعض الأوقات.
+
</syntaxhighlight>يأخذ باني الوعد دالّة تنفيذية تسمح لنا بحل أو رفض كائن الوعد يدويًّا. لما كان فشل الدالة <code>setTimeout()‎</code> مستبعدًا، فقد أهملنا وضع رفضٍ في هذه الحالة.
  
 +
== التركيب ==
 +
يُعدّ <code>[[JavaScript/Promise/resolve|Promise.resolve()‎]]</code> و <code>[[JavaScript/Promise/reject|Promise.reject()‎]]</code> اختصارين لإنشاء وعدٍ محلولٍ بالفعل أو مرفوضٍ بالفعل على التّرتيب يدويًا. نجد هذا مفيدًا جدًا في بعض الأوقات.
  
يُعرّف [`Promise.all()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all ) و [`Promise.race()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race ) على أنّهما أداتا تركيب لتنفيذ العمليات غير المتزامنة على التّوازي.
+
يُعرّف <code>[[JavaScript/Promise/all|Promise.all()]]</code> و <code>[[JavaScript/Promise/race|Promise.race()]]</code> على أنّهما أداتا تركيب لتنفيذ العمليات غير المتزامنة على التّوازي.
  
يمكننا بدء العمليات على التوازي وانتظار انتهائها كما يلي:
+
يمكننا بدء العمليات على التوازي وانتظار انتهائها كما يلي:<syntaxhighlight lang="javascript">
```js
 
 
Promise.all([func1(), func2(), func3()])
 
Promise.all([func1(), func2(), func3()])
 
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });
 
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });
```
 
  
يمكن القيام بالتّركيب التًسلسلي بالاستخدام الحذق للغة JavaScript:
+
</syntaxhighlight>يمكن القيام بالتّركيب التًسلسلي بالاستخدام الحذق للغة JavaScript:<syntaxhighlight lang="javascript">
```js
 
 
[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
 
[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
 
.then(result3 => { /* use result3 */ });
 
.then(result3 => { /* use result3 */ });
```
 
 
 
نختزل مصفوفة الدّوال غير المتزامنة إلى سلسلة وعود مكافئة للتّالي:`Promise.resolve().then(func1).then(func2).then(func3);`
 
 
يمكن تحويل هذا إلى دالة تركيب قابلة لإعادة الاستخدام. هذا الأمر شائع في البرمجة الوظيفيّة.
 
  
```js
+
</syntaxhighlight>نختزل مصفوفة الدّوال غير المتزامنة إلى سلسلة وعود مكافئة للتّالي: <syntaxhighlight lang="javascript">
 +
Promise.resolve().then(func1).then(func2).then(func3);
 +
</syntaxhighlight>يمكن تحويل هذا إلى دالة تركيب قابلة لإعادة الاستخدام. هذا الأمر شائع في البرمجة الوظيفيّة.<syntaxhighlight lang="javascript">
 
const applyAsync = (acc,val) => acc.then(val);
 
const applyAsync = (acc,val) => acc.then(val);
 
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
 
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
```
 
 
 
ستقبل الدّالة `composeAsync()‎` أيّ عدد من الدّوال كمتغيّرات وستُعيد دالة جديدة تقبل تمرير القيمة الأوليّة عبر أنبوب (Pipeline).
 
  
```js
+
</syntaxhighlight>ستقبل الدّالة <code>composeAsync()‎</code> أيّ عدد من الدّوال كمتغيّرات وستُعيد دالة جديدة تقبل تمرير القيمة الأوليّة عبر أنبوب (Pipeline).<syntaxhighlight lang="javascript">
 
const transformData = composeAsync(func1, func2, func3);
 
const transformData = composeAsync(func1, func2, func3);
 
const result3 = transformData(data);
 
const result3 = transformData(data);
```
 
 
يمكن القيام بالتّركيب التّسلسلي بسهولةٍ أكبر من خلال `async/await` في ECMAScript 2017.
 
  
```js
+
</syntaxhighlight>يمكن القيام بالتّركيب التّسلسلي بسهولةٍ أكبر من خلال <code>async/await</code> في ECMAScript 2017.<syntaxhighlight lang="javascript">
 
let result;
 
let result;
 
for (const f of [func1, func2, func3]) {
 
for (const f of [func1, func2, func3]) {
 
   result = await f(result);
 
   result = await f(result);
 
}
 
}
/* use last result (i.e. result3) */
+
/* (result3 استخدم آخر نتيجة (أي */
```
 
  
## التّوقيت
+
</syntaxhighlight>
  
لا تُستدعى الدّوال الممرّرة إلى [()then](https://wiki.hsoub.com/JavaScript/Promise/then) على نحو تزامنيّ مطلقاُ حتى بوجود وعدٍ محلول بالفعل وذلك لتفادي النّتائج غير المتوقّعة.
+
== التوقيت ==
 
+
لا تُستدعى الدّوال الممرّرة إلى [[JavaScript/Promise/then|<code>()then</code>]] على نحو تزامنيّ مطلقاُ حتى بوجود وعدٍ محلول بالفعل وذلك لتفادي النّتائج غير المتوقّعة.<syntaxhighlight lang="javascript">
```js
 
 
Promise.resolve().then(() => console.log(2));
 
Promise.resolve().then(() => console.log(2));
 
console.log(1); // 1, 2
 
console.log(1); // 1, 2
```
 
  
تُوضع الدّالة الممرّرة في طابور مهام مُصغّرة microtask queue عوضًا عن تنفيذها مباشرةً، هذا يعني أنّها تُنفّذ لاحقًأ عندما يفرغ الطّابور في نهاية التّنفيذ الحالي لحلقة أحداث JavaScript. لا يستغرق هذا الأمر وقتًا يذكر.
+
</syntaxhighlight>تُوضع الدّالة الممرّرة في طابور مهام مُصغّرة microtask queue عوضًا عن تنفيذها مباشرةً، هذا يعني أنّها تُنفّذ لاحقًأ عندما يفرغ الطّابور في نهاية التّنفيذ الحالي لحلقة أحداث JavaScript. لا يستغرق هذا الأمر وقتًا يذكر.<syntaxhighlight lang="javascript">
```js
 
 
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
 
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
  
سطر 309: سطر 225:
 
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
 
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
 
console.log(1); // 1, 2, 3, 4
 
console.log(1); // 1, 2, 3, 4
```
 
  
## التّداخل
+
</syntaxhighlight>
  
 +
== التداخل ==
 
يُفضّل الإبقاء على سلاسل الوعود البسيطة مسطّحةً بدون تداخل على اعتبار أنّ التّداخل قد يكون نتيجة لتركيب غير متقن. يُمكنك الاطلاع على بعض الأخطاء الشّائعة أدناه.
 
يُفضّل الإبقاء على سلاسل الوعود البسيطة مسطّحةً بدون تداخل على اعتبار أنّ التّداخل قد يكون نتيجة لتركيب غير متقن. يُمكنك الاطلاع على بعض الأخطاء الشّائعة أدناه.
  
يُعرّف التّداخل على أنّه بنية تحكميّة للحدّ من نطاق عبارات `catch` . بكلمات أدقّ، تلتقط `catch` المتداخلة الأخطاء الواردة في نطاقها وما دونه فقط، ولا تلتقط الأخطاء الواردة في نطاق أعلى في السّلسلة خارج النطاق المتداخل. يمنح التدّاخل عند استثماره على نحو صحيح دقّةً أكبر في التصحيح بعد الخطأ.
+
يُعرّف التّداخل على أنّه بنية تحكميّة للحدّ من نطاق عبارات <code>catch</code>. بكلمات أدقّ، تلتقط <code>catch</code> المتداخلة الأخطاء الواردة في نطاقها وما دونه فقط، ولا تلتقط الأخطاء الواردة في نطاق أعلى في السّلسلة خارج النطاق المتداخل. يمنح التدّاخل عند استثماره على نحو صحيح دقّةً أكبر في التصحيح بعد الخطأ.<syntaxhighlight lang="javascript">
 
 
```js
 
 
doSomethingCritical()
 
doSomethingCritical()
 
.then(result => doSomethingOptional()
 
.then(result => doSomethingOptional()
سطر 324: سطر 238:
 
.then(() => moreCriticalStuff())
 
.then(() => moreCriticalStuff())
 
.catch(e => console.log("Critical failure: " + e.message));
 
.catch(e => console.log("Critical failure: " + e.message));
```
 
 
 
 
لاحظ أنّ الخطوات الاختياريّة هنا متداخلة؛ لا علاقة للأمر بالإزاحة إنّما بالتّموضع المُتقلقل لكلٍّ من`)` و `(` الخارجيّة حولها.
 
تلتقط عبارة `catch` الدّاخليّة المُحيّدة حالات الفشل من `doSomethingOptional()` و `doSomethingExtraNice()` فقط. تُتابع الشّيفرة البرمجيّة بعد ذلك مع `moreCriticalStuff()`.
 
من المهم الانتباه إلى أنّه عند فشل `doSomethingCritical()` فإن  `catch` الخارجيّة النهائيّة فقط هي من يلتقط الخطأ.
 
  
## أخطاء شائعة
+
</syntaxhighlight>لاحظ أنّ الخطوات الاختياريّة هنا متداخلة؛ لا علاقة للأمر بالإزاحة إنّما بالتّموضع المُتقلقل لكلٍّ من <code>)</code> و <code>(</code> الخارجيّة حولها. تلتقط عبارة <code>catch</code> الدّاخليّة المُحيّدة حالات الفشل من <code>doSomethingOptional()‎</code> و <code>doSomethingExtraNice()‎</code> فقط. تُتابع الشّيفرة البرمجيّة بعد ذلك مع <code>moreCriticalStuff()‎</code>. من المهم الانتباه إلى أنّه عند فشل <code>doSomethingCritical()‎</code> فإن  <code>catch</code> الخارجيّة النهائيّة فقط هي من يلتقط الخطأ.
  
تجنّب الوقوع في الأخطاء الشائعة عند تركيب سلسلة وعود. يظهر عدد من هذه الأخطاء في المثال التّالي:
+
== أخطاء شائعة ==
```js
+
تجنّب الوقوع في الأخطاء الشائعة عند تركيب سلسلة وعود. يظهر عدد من هذه الأخطاء في المثال التّالي:<syntaxhighlight lang="javascript">
 
// مثال خاطئ! حاول اكتشاف الأخطاء فيه.
 
// مثال خاطئ! حاول اكتشاف الأخطاء فيه.
  
سطر 343: سطر 250:
 
}).then(() => doFourthThing());
 
}).then(() => doFourthThing());
 
// Catch إغفال إنهاء السّلسلة باستخدام  
 
// Catch إغفال إنهاء السّلسلة باستخدام  
```
+
 
الخطأ الأوّل هو عدم سلسلة الأشياء معًا بشكلٍ سليم. يحدث هذا عندما نقوم بإنشاء وعدٍ جديد لكننا ننسى إرجاعه.  يترتّب على هذا كسرُ السلسلة، أو بكلمات أدق، يصبح لدينا سلسلتان مستقلّتان تتسابقان في التّنفيذ. هذا يعني أنّ الدّالة`doFourthThing()` لن تنتظر انتهاء `doSomethingElse()` أو `doThirdThing()بل ستُنفّذ على التوازي معهما، هذا الأمر غير مرغوب به غالباَ. تتوّفر السلاسل المنفصلة على معالجات أخطاء منفصلة ممّا يؤدّي إلى أخطاء غير مُلتقَطة.
+
</syntaxhighlight>الخطأ الأوّل هو عدم سلسلة الأشياء معًا بشكلٍ سليم. يحدث هذا عندما نقوم بإنشاء وعدٍ جديد لكننا ننسى إرجاعه.  يترتّب على هذا كسرُ السلسلة، أو بكلمات أدق، يصبح لدينا سلسلتان مستقلّتان تتسابقان في التّنفيذ. هذا يعني أنّ الدّالة<code>doFourthThing()‎</code> لن تنتظر انتهاء <code>doSomethingElse()‎</code> أو <code>doThirdThing()‎</code> بل ستُنفّذ على التوازي معهما، هذا الأمر غير مرغوب به غالباَ. تتوّفر السلاسل المنفصلة على معالجات أخطاء منفصلة ممّا يؤدّي إلى أخطاء غير مُلتقَطة.
  
 
الخطأ الثّاني هو التّداخل غير الضّروري والذي يفسح المجال بدوره للخطأ الأوّل. يحِدُّ التداخل أيضًا من نطاق معالجات الأخطاء الدّاخليّة. يؤدّي هذا الأمر، إن لم يكُ مقصودًا، إلى أخطاء غير مُلتقَطة.
 
الخطأ الثّاني هو التّداخل غير الضّروري والذي يفسح المجال بدوره للخطأ الأوّل. يحِدُّ التداخل أيضًا من نطاق معالجات الأخطاء الدّاخليّة. يؤدّي هذا الأمر، إن لم يكُ مقصودًا، إلى أخطاء غير مُلتقَطة.
  
يُعتبر [النّموذج المضاد للباني (promise constructor anti-pattern)](https://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it) والذي يجمع بين التّداخل والاستخدام الفائض لباني الوعد لتغليف الشّيفرة البرمجيّة التي تستخدم الوعود بالفعل أحد أشكال هذا الخطأ.
+
يُعتبر النّموذج المضاد للباني (promise constructor anti-pattern) والذي يجمع بين التّداخل والاستخدام الفائض لباني الوعد لتغليف الشّيفرة البرمجيّة التي تستخدم الوعود بالفعل أحد أشكال هذا الخطأ.
 
 
 
 
 
 
الخطأ الثّالث هو إغفال إنهاء السلاسل بواسطة `catch`. تُؤدي سلاسل الوعود غير المنتهية إلى حالات رفض وعودٍ غير مُلتقَطة في معظم المتصفّحات.
 
  
يُستحسَن دومًا كقاعدة عامّة إما الإرجاع أو إنهاء سلاسل الوعود. قُم بإرجاع أيّ وعدٍ جديد حال حصولك عليه. للتوضيح:
+
الخطأ الثّالث هو إغفال إنهاء السلاسل بواسطة <code>catch</code>. تُؤدي سلاسل الوعود غير المنتهية إلى حالات رفض وعودٍ غير مُلتقَطة في معظم المتصفّحات.
  
```js
+
يُستحسَن دومًا كقاعدة عامّة إما الإرجاع أو إنهاء سلاسل الوعود. قُم بإرجاع أيّ وعدٍ جديد حال حصولك عليه. للتوضيح:<syntaxhighlight lang="javascript">
 
doSomething()
 
doSomething()
 
.then(function(result) {
 
.then(function(result) {
سطر 364: سطر 267:
 
.then(() => doFourthThing())
 
.then(() => doFourthThing())
 
.catch(error => console.log(error));
 
.catch(error => console.log(error));
```
 
 
لاحظ أنّ `() => x`  يُمثّل اختزالًا للتركيب `() => { return x; }`.
 
 
أصبح لدينا الآن سلسلةٌ وحيدة حتميّة ذات معالجة أخطاء سليمة. تُعالج معظم، إن لم تكن جميع، هذه المشاكل باستخدام `async/await`  مع مراعاة تذكّر الكلمة المفتاحية [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)  في هذه الصّياغة.
 
  
 +
</syntaxhighlight>لاحظ أنّ ‎<code>() => x</code>  يُمثّل اختزالًا للتركيب ‎<code>() => { return x; }‎</code>.
  
 +
أصبح لدينا الآن سلسلةٌ وحيدة حتميّة ذات معالجة أخطاء سليمة. تُعالج معظم، إن لم تكن جميع، هذه المشاكل باستخدام <code>async/await</code>  مع مراعاة تذكّر الكلمة المفتاحية <code>await</code>  في هذه الصّياغة.
  
## المصادر
+
== المصادر ==
* [صفحة Using promises في توثيق MDN الرسمي](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises).
+
* [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises صفحة Using promises في توثيق MDN الرسمي].

مراجعة 15:48، 4 سبتمبر 2019


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

يمكننا القول أنّ «الوعد» (promise) في جوهره ما هو إلّا كائنٌ مُعادٌ تقوم بإرفاق ردود النّداء (Callbacks) معه عوضًا عن تمريرها إلى الدّالّة.

تخيّل أنّ لدينا الدّالّة createAudioFileAsync()‎ والتي تُولّد بطريقةٍ غير متزامنةٍ ملفًّا صوتيًّا انطلاقًا من ضبط الملف الصوتي ودالّتي رد نداء تُستدعَى الأولى عند نجاح إنشاء الملف الصوتي والثّانية عند حدوث أخطاء. إليك شيفرة تستخدم الدّالّة createAudioFileAsync()‎:

function successCallback(result) {
  console.log("Audio file ready at URL: " + result);
}

function failureCallback(error) {
  console.log("Error generating audio file: " + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback);

تُعيد الدّوالُّ الحديثة وعدًا يمكنك إرفاق ردود النّداء معه عوضًا عن ذلك. إذا ما أُعيدت كتابة الدّالّة createAudioFileAsync()‎ لتُعيدَ وعدًا، يصبح استخدامها بهذه البساطة:

createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

يُمثّل ما سبق اختزالًا لما يلي:

const promise = createAudioFileAsync(audioSettings); 
promise.then(successCallback, failureCallback);

نُطلق على هذه الحالة استدعاء دالّة استدعاءً غير متزامن. يمنحنا هذا الاصطلاح عددًا من الميّزات سنتطّرق لكل منها على حدة.

الضمانات

على نقيض ردود النّداء المُمَرّرة ذات النمط القديم، يأتي Promise مع بعض الضمانات:

  • لن تُطلَب ردود النّداء قبل إتمام التنفيذ الحالي لحلقة الأحداث في JavaScript.
  • تًطلب ردود النداء المضافة باستخدام then()‎ حتى بعد نجاح أو فشل العملية غير المتزامنة وفق ما ذكر أعلاه.
  • يُمكن إضافة عدة ردود نداء من خلال استدعاء then()‎ عدة مرات. تُنفّذ جميع ردود النداء واحدًا تلو الآخر بنفس الترتيب الذي أدرجت به.

يُقدم استخدام الوعود العديد من الميزات الرائعة، نذكرالآن منها استخدام السّلسَلة (chaining).

السّلسََلة (الاستدعاء المتسلسل)

يُعدُّ التنفيذ التّعاقبيُّ لعمليّتَين غير متزامنتَين أو أكثر حاجةً مشتركةً لدى العديد من المُطوّرين، حين تقتضي الضّرورة أن تبدأ العمليّة اللّاحقة فور نجاح سابقتها ومعتمدةً على النّتيجة من تلك الأخيرة. نُنجز هذا من خلال إنشاء سلسلة من الوعود (promise chain).

هنا يكمن الإبداع: تٌعيد الدّالّة then()‎ كائن Promise جديد مختلفًا عن الأصليّ:

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

أو:

const promise2 = doSomething().then(successCallback, failureCallback);

لا يُمثّل الكائن promise2 إتمام doSomething()‎ فحسب، إنّما يشمل ما قمت بتمريره سواء successCallback أو failureCallback؛ الأمر الذي قد يُمثّل دوالًا غير متزامنة أخرى تقوم بدورها بإرجاع وعد. في هذه الحالة، تُوضع أيّة ردود نداء مضافة إلى promise2 في طابور وذلك خلف الوعد المُعاد إمّا من قبل successCallback أو failureCallback.

يُمثّل كل وعدٍ بصفةِ أساسيّةٍ إتمام خطوة غير متزامنة أخرى في السّلسلة.

أدى تنفيذ عدّة عمليّات غير متزامنة على التّسلسل في السّابق إلى حالة هرم الموت (pyramid of doom):

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

نُرفق كحلّ بديل في الدّوال الحديثة ردود النداء مع الوعود المُعادة مما يُشكّل سلسلة وعود:

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

إضافة المُتغيّرات إلى then أمرٌ غير إلزاميّ، أمّا catch(failureCallback)‎ فهي اختزال للدّالّة then(null, failureCallback)‎ وقد تُصادِف في المقابل هذا الأمر مُمثّلًا بدوال سهميّة.

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

ملاحظة مهمة: قُم دائمًا بإرجاع نتائج وإلّا لن تقوم ردود النداء بالتقاط نتيجة الوعد السّابق (تُمثّل في الدّوال السّهميّة ‎() => x اختزالًا للتّركيب ‎() => { return x; }‎).

استخدام السّلاسل بعد catch

يُمكن استخدام السّلاسل بعد الفشل في حالة catch، إذ يفيد هذا في إنجاز إجراءات جديدة حتى بعد فشل إجراء في سلسلة الاستدعاء. إليك المثال التّالي:

new Promise((resolve, reject) => {
    console.log('Initial');

    resolve();
})
.then(() => {
    throw new Error('Something failed');
        
    console.log('Do this');
})
.catch(() => {
    console.log('Do that');
})
.then(() => {
    console.log('Do this, no matter what happened before');
});

تُخرج الشّيفرة السّابقة النّص التّالي:

Initial
Do that
Do this, no matter what happened before

ملاحظة: لا يُعرض النص "Do this" لأن الخطاً "Something failed" تسبّب بحدوث رفض.

انتشار الخطأ

قد تعود إلى ذهنك رؤية الدّالّة failureCallback ثلاث مرّاتٍ في هرم الموت أعلاه بالمقارنة مع ورودها مّرةً واحدة في نهاية سلسلة وعود:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);

تتوقّف سلسلة الوعود عند وجود استثناء لتبحث عوض ذلك أدناه عن معالجات catch. نُمذج هذا الأمر على نحوٍ مماثل للغاية لعمل الشّيفرة البرمجيّة المتزامنة.

try {
  const result = syncDoSomething();
  const newResult = syncDoSomethingElse(result);
  const finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

يبلغُ هذا التماثل ذروته عند استخدام التجميل اللّغوي async/await في ECMAScript 2017:

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

يعتمد الأمر تمامًا على الوعود، على سبيل المثال doSomething()‎ هي نفسها الدّالّة السّابقة. إذا أردت، يمكنك قراءة المزيد حول هذه الصّياغة.

تُقدّم الوعود حلًّا لخللٍ جوهريٍّ في استدعاء هرم الموت، ذلك بالتقاط (catch) جميعَ الأخطاء حتى الاستثناءات المرميّة منها وأخطاء البرمجة. هذا الأمر في غاية الأهميّة للتركيب الوظيفيّ للعمليات غير المتزامنة.

أحداث رفض الوعود

عند رفض وعدٍ، يُرسَل أحد حدثين اثنين إلى النّطاق العمومي (يكون هذا النّطاق غالباً إما النّافذة window أو عند الاستخدام في عامل وِيب، العامل Worker أو واجهة أخرى مبنيّة على عامل). الحدثان المذكوران أعلاه هما:

rejectionhandled

يُرسل عندما يُرفض الوعد بعد أن يعالج الخطأ من قبل دالّة reject الخاصّة بالمنفّذ.

unhandledrejection

يُرسل عندما يُرفض الوعد ولا يتوفّر هناك معالج رفض.

في كلتي الحالتين، يتوفّر الحدث من النّوع PromiseRejectionEvent على promise كعضوٍ للإشارة إلى الوعد المرفوض وعلى واصفة reason لتبيان السّبب وراء رفض هذا الوعد.

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

للوقوف على إحدى الفوائد الهامّة لما ذكر أعلاه نعرض التّالي: من الشّائع عند كتابة شيفرة برمجية لبيئة Node.js أن تكون الوحدات التي تُضمّنها في مشروعك ذات وعودٍ مرفوضة وغير مدَاوَلة. تُسجّل هذه الوعود في الكونسول من قبل مُشغِّل Node الآني (Node runtime). يمكنك التقاطها في شيفرتك البرمجيّة للتّحليل والمعالجة أو لتفادي بعثرتها في خرج شيفرتك وذلك ومن خلال إضافة معالج للحدث unhandledrejection كما يلي:

window.addEventListener("unhandledrejection", event => {
 /* يُمكنك أن تبدأ هنا بإضافة الشّيفرة البرمجيّة
event.promise لفحص الوعد الذي يُحدّده  
event.reason والسّبب في 
 */
  event.preventDefault();
}, false);

باستدعاء تابع preventDefault()‎ الخاصّة بالحدث، تقوم بتوجيه مُشغل JavaScript الآني (JavaScript runtime) إلى عدم اتّخاذ إجراءه الافتراضي عندما لا تُعالج الوعود المرفوضة. عادةً ما يتضمن الإجراء الافتراضي هذا تسجيل الخطأ في الطرفية وهذه هي الحالة فعلًا في Node.

يتوجّب عليك في الحالة المثلى فحص الوعود المرفوضة قبل نبذ الأحداث هذه حرصًا أن يكون أيّ منها علّة فعليّة في الشّيفرة البرمجيّة.

إنشاء الوعد حول واجهة برمجة تطبيقات API ذات ردِّ نداء قديم

يمكن إنشاء وعدٍ من الصّفر باستخدام بانيه. نحتاج هذا الأمر عند تغليف واجهات برمجة تطبيقات قديمة.

تُعيد جميع الدّوال غير المتزامنة وعودًا في الحالة المثلى. لكن لسوء الحظّ، لا تزال بعض واجهات برمجة التطبيقات تنتظر تمرير نتيجة الاستدعاء من نجاح و/أو فشل وفق الطّريقة المتّبعة قديمًا. يتجلّى هذا بأوضح صوره في الدّالة setTimeout()‎.

setTimeout(() => saySomething("10 seconds passed"), 10000);

يتسبب المزج بين ردود النداء ذات النمط القديم والوعود بالكثير من المشاكل. لا يوجد ما يلتقط الخطأ إذا ما فشل saySomething()‎ أو احتوى على خطأ برمجي وتقع المسؤولية في هذا على عاتق setTimeout . لحسن الحظ، يمكننا تغليف setTimeout ضمن الوعد. الممارسة المثلى في هذه الحالة هي تغليف الدوال الإشكالية في أدنى مستوى ممكن ومن ثم عدم استدعائها مباشرة بعد ذلك:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

يأخذ باني الوعد دالّة تنفيذية تسمح لنا بحل أو رفض كائن الوعد يدويًّا. لما كان فشل الدالة setTimeout()‎ مستبعدًا، فقد أهملنا وضع رفضٍ في هذه الحالة.

التركيب

يُعدّ Promise.resolve()‎ و Promise.reject()‎ اختصارين لإنشاء وعدٍ محلولٍ بالفعل أو مرفوضٍ بالفعل على التّرتيب يدويًا. نجد هذا مفيدًا جدًا في بعض الأوقات.

يُعرّف Promise.all()‎ و Promise.race()‎ على أنّهما أداتا تركيب لتنفيذ العمليات غير المتزامنة على التّوازي.

يمكننا بدء العمليات على التوازي وانتظار انتهائها كما يلي:

Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });

يمكن القيام بالتّركيب التًسلسلي بالاستخدام الحذق للغة JavaScript:

[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });

نختزل مصفوفة الدّوال غير المتزامنة إلى سلسلة وعود مكافئة للتّالي:

Promise.resolve().then(func1).then(func2).then(func3);

يمكن تحويل هذا إلى دالة تركيب قابلة لإعادة الاستخدام. هذا الأمر شائع في البرمجة الوظيفيّة.

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

ستقبل الدّالة composeAsync()‎ أيّ عدد من الدّوال كمتغيّرات وستُعيد دالة جديدة تقبل تمرير القيمة الأوليّة عبر أنبوب (Pipeline).

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

يمكن القيام بالتّركيب التّسلسلي بسهولةٍ أكبر من خلال async/await في ECMAScript 2017.

let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}
/* (result3 استخدم آخر نتيجة (أي */

التوقيت

لا تُستدعى الدّوال الممرّرة إلى ()then على نحو تزامنيّ مطلقاُ حتى بوجود وعدٍ محلول بالفعل وذلك لتفادي النّتائج غير المتوقّعة.

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

تُوضع الدّالة الممرّرة في طابور مهام مُصغّرة microtask queue عوضًا عن تنفيذها مباشرةً، هذا يعني أنّها تُنفّذ لاحقًأ عندما يفرغ الطّابور في نهاية التّنفيذ الحالي لحلقة أحداث JavaScript. لا يستغرق هذا الأمر وقتًا يذكر.

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

التداخل

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

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

doSomethingCritical()
.then(result => doSomethingOptional()
  .then(optionalResult => doSomethingExtraNice(optionalResult))
  .catch(e => {})) // .وتابع doSomethingOptional() تجاهل هذا إذا ما فشل تنفيذ
.then(() => moreCriticalStuff())
.catch(e => console.log("Critical failure: " + e.message));

لاحظ أنّ الخطوات الاختياريّة هنا متداخلة؛ لا علاقة للأمر بالإزاحة إنّما بالتّموضع المُتقلقل لكلٍّ من ) و ( الخارجيّة حولها. تلتقط عبارة catch الدّاخليّة المُحيّدة حالات الفشل من doSomethingOptional()‎ و doSomethingExtraNice()‎ فقط. تُتابع الشّيفرة البرمجيّة بعد ذلك مع moreCriticalStuff()‎. من المهم الانتباه إلى أنّه عند فشل doSomethingCritical()‎ فإن catch الخارجيّة النهائيّة فقط هي من يلتقط الخطأ.

أخطاء شائعة

تجنّب الوقوع في الأخطاء الشائعة عند تركيب سلسلة وعود. يظهر عدد من هذه الأخطاء في المثال التّالي:

// مثال خاطئ! حاول اكتشاف الأخطاء فيه.

doSomething().then(function(result) {
  doSomethingElse(result) // إغفال إعادة الوعد من السلسلة الداخليّة + تداخل غير ضروري
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Catch إغفال إنهاء السّلسلة باستخدام

الخطأ الأوّل هو عدم سلسلة الأشياء معًا بشكلٍ سليم. يحدث هذا عندما نقوم بإنشاء وعدٍ جديد لكننا ننسى إرجاعه. يترتّب على هذا كسرُ السلسلة، أو بكلمات أدق، يصبح لدينا سلسلتان مستقلّتان تتسابقان في التّنفيذ. هذا يعني أنّ الدّالةdoFourthThing()‎ لن تنتظر انتهاء doSomethingElse()‎ أو doThirdThing()‎ بل ستُنفّذ على التوازي معهما، هذا الأمر غير مرغوب به غالباَ. تتوّفر السلاسل المنفصلة على معالجات أخطاء منفصلة ممّا يؤدّي إلى أخطاء غير مُلتقَطة.

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

يُعتبر النّموذج المضاد للباني (promise constructor anti-pattern) والذي يجمع بين التّداخل والاستخدام الفائض لباني الوعد لتغليف الشّيفرة البرمجيّة التي تستخدم الوعود بالفعل أحد أشكال هذا الخطأ.

الخطأ الثّالث هو إغفال إنهاء السلاسل بواسطة catch. تُؤدي سلاسل الوعود غير المنتهية إلى حالات رفض وعودٍ غير مُلتقَطة في معظم المتصفّحات.

يُستحسَن دومًا كقاعدة عامّة إما الإرجاع أو إنهاء سلاسل الوعود. قُم بإرجاع أيّ وعدٍ جديد حال حصولك عليه. للتوضيح:

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));

لاحظ أنّ ‎() => x يُمثّل اختزالًا للتركيب ‎() => { return x; }‎.

أصبح لدينا الآن سلسلةٌ وحيدة حتميّة ذات معالجة أخطاء سليمة. تُعالج معظم، إن لم تكن جميع، هذه المشاكل باستخدام async/await مع مراعاة تذكّر الكلمة المفتاحية await في هذه الصّياغة.

المصادر