تخيل أنك مغني كبير، والمعجبون يسألون ليلًا ونهارًا عن أغنيتك القادمة.
للحصول على بعض الراحة، تعد بإرسالها إليهم عند نشرها. أنت تعطي معجبيك قائمة. ويمكنهم ملء عناوين البريد الإلكتروني الخاصة بهم، بحيث تتلقى الأغنية جميع الأطراف المشتركة على الفور عندما تصبح متاحة. وحتى إذا حدث خطأ ما، على سبيل المثال، حريق في الاستوديو، بحيث لا تتمكن من نشر الأغنية، فسيتم إخطارهم بذلك.
الجميع سعداء: أنت، لأن الناس لم يعودوا يزاحمونك، والمعجبون، لأنهم لن يفوتوا الأغنية.
هذا تشبيه واقعي للأشياء التي غالبًا ما نواجهها في البرمجة:
"إنتاج التعليمات البرمجية" الذي يفعل شيئًا ما ويستغرق وقتًا. على سبيل المثال، بعض التعليمات البرمجية التي تقوم بتحميل البيانات عبر الشبكة. هذا هو "المغني".
"الكود المستهلك" الذي يريد نتيجة "الكود المنتج" بمجرد أن يصبح جاهزًا. قد تحتاج العديد من الوظائف إلى هذه النتيجة. هؤلاء هم "المشجعون".
الوعد هو كائن جافا سكريبت خاص يربط بين "الكود المنتج" و"الكود المستهلك" معًا. من حيث القياس لدينا: هذه هي "قائمة الاشتراك". يستغرق "إنتاج التعليمات البرمجية" أي وقت يحتاجه لإنتاج النتيجة الموعودة، ويجعل "الوعد" هذه النتيجة متاحة لجميع التعليمات البرمجية المشتركة عندما تكون جاهزة.
التشبيه ليس دقيقًا إلى حد كبير، لأن وعود JavaScript أكثر تعقيدًا من قائمة الاشتراك البسيطة: فهي تحتوي على ميزات وقيود إضافية. لكن لا بأس أن نبدأ.
بناء جملة المنشئ لكائن الوعد هو:
دع الوعد = وعد جديد (وظيفة (حل، رفض) { // المنفذ (رمز الإنتاج، "المغني") });
الوظيفة التي تم تمريرها إلى new Promise
تسمى المنفذ . عند إنشاء new Promise
، يتم تشغيل المنفذ تلقائيًا. أنه يحتوي على التعليمات البرمجية المنتجة التي ينبغي أن تنتج النتيجة في نهاية المطاف. ومن حيث التشبيه أعلاه: فالمنفذ هو "المغني".
وسيطاتها resolve
reject
هي عمليات رد اتصال توفرها JavaScript نفسها. الكود الخاص بنا موجود فقط داخل المنفذ.
عندما يحصل المنفذ على النتيجة، لا يهم، سواء كان ذلك قريبًا أو متأخرًا، يجب عليه استدعاء أحد عمليات الاسترجاعات التالية:
resolve(value)
- إذا تم الانتهاء من المهمة بنجاح، مع value
النتيجة .
reject(error)
- إذا حدث خطأ، فإن error
هو كائن الخطأ.
لتلخيص ذلك: يعمل المنفذ تلقائيًا ويحاول أداء مهمة ما. عند الانتهاء من المحاولة، فإنه يستدعي resolve
إذا كانت ناجحة أو reject
إذا كان هناك خطأ.
يحتوي كائن promise
الذي أرجعه منشئ new Promise
على هذه الخصائص الداخلية:
state
- في البداية "pending"
، ثم تتغير إما إلى "fulfilled"
عند استدعاء resolve
أو "rejected"
عند استدعاء reject
.
result
- undefined
في البداية، ثم تتغير إلى value
عند استدعاء resolve(value)
أو حدوث error
عند استدعاء reject(error)
.
لذلك يقوم المنفذ في النهاية بنقل promise
إلى إحدى هذه الحالات:
سنرى لاحقًا كيف يمكن لـ "المعجبين" الاشتراك في هذه التغييرات.
فيما يلي مثال لمنشئ الوعد ووظيفة منفذ بسيطة مع "إنتاج تعليمات برمجية" تستغرق وقتًا (عبر setTimeout
):
دع الوعد = وعد جديد (وظيفة (حل، رفض) { // يتم تنفيذ الوظيفة تلقائيًا عند إنشاء الوعد // بعد إشارة ثانية واحدة إلى أن المهمة قد انتهت، تكون النتيجة "تم" setTimeout(() => Resolve("done"), 1000); });
يمكننا أن نرى شيئين عن طريق تشغيل الكود أعلاه:
يتم استدعاء المنفذ تلقائيًا وفورًا (بواسطة new Promise
).
يتلقى المنفذ وسيطتين: resolve
reject
. يتم تعريف هذه الوظائف مسبقًا بواسطة محرك JavaScript، لذلك لا نحتاج إلى إنشائها. يجب أن نتصل بواحد منهم فقط عندما يكون جاهزًا.
بعد ثانية واحدة من "المعالجة"، يستدعي المُنفِّذ resolve("done")
لإنتاج النتيجة. يؤدي هذا إلى تغيير حالة كائن promise
:
وكان ذلك مثالاً على إكمال المهمة بنجاح، "الوعد الذي تم الوفاء به".
والآن مثال على رفض المنفذ للوعد بالخطأ:
دع الوعد = وعد جديد (وظيفة (حل، رفض) { // بعد إشارة ثانية واحدة إلى انتهاء المهمة مع وجود خطأ setTimeout(() =>رفض(new Error("عفوا!")), 1000); });
يؤدي استدعاء reject(...)
إلى نقل كائن الوعد إلى الحالة "rejected"
:
لتلخيص ذلك، يجب على المنفذ تنفيذ مهمة (عادة ما تستغرق وقتًا) ثم استدعاء resolve
أو reject
لتغيير حالة كائن الوعد المقابل.
يُطلق على الوعد الذي يتم حله أو رفضه "تسوية"، على عكس الوعد "المعلق" في البداية.
يمكن أن يكون هناك نتيجة واحدة أو خطأ واحد فقط
يجب على المنفذ أن يستدعي resolve
واحدًا فقط أو reject
واحدًا فقط. أي تغيير في الحالة نهائي.
يتم تجاهل جميع دعوات resolve
reject
الأخرى:
دع الوعد = وعد جديد (وظيفة (حل، رفض) { حل("تم"); رفض(خطأ جديد("...")); // تم التجاهل setTimeout(() => العزم("...")); // تم التجاهل });
والفكرة هي أن العمل الذي يقوم به المنفذ قد يكون له نتيجة واحدة فقط أو خطأ واحد.
أيضًا، يتوقع resolve
/ reject
وسيطة واحدة فقط (أو لا شيء) وسيتجاهل الوسائط الإضافية.
الرفض باستخدام كائنات Error
في حالة حدوث خطأ ما، يجب على المنفذ استدعاء reject
. يمكن القيام بذلك باستخدام أي نوع من الوسائط (تمامًا مثل resolve
). لكن يوصى باستخدام كائنات Error
(أو الكائنات الموروثة من Error
). والسبب في ذلك سوف يصبح واضحا قريبا.
استدعاء resolve
/ reject
على الفور
من الناحية العملية، عادةً ما يقوم المنفذ بتنفيذ شيء ما بشكل غير متزامن ويستدعي resolve
/ reject
بعد مرور بعض الوقت، ولكن ليس من الضروري القيام بذلك. يمكننا أيضًا استدعاء resolve
أو reject
فورًا، مثل هذا:
دع الوعد = وعد جديد (وظيفة (حل، رفض) { // لا نأخذ وقتنا للقيام بهذه المهمة العزم(123); // أعط النتيجة على الفور: 123 });
على سبيل المثال، قد يحدث هذا عندما نبدأ في القيام بمهمة ما ولكن بعد ذلك نرى أن كل شيء قد اكتمل بالفعل وتم تخزينه مؤقتًا.
هذا جيّد. لدينا على الفور وعد حل.
state
result
داخلية
state
الخصائص result
كائن Promise داخلية. لا يمكننا الوصول إليهم مباشرة. يمكننا استخدام الطرق .then
/ .catch
/ .finally
لذلك. تم وصفها أدناه.
يعمل كائن الوعد كحلقة وصل بين المنفذ ("الكود المنتج" أو "المغني") والوظائف المستهلكة ("المعجبين")، والتي ستتلقى النتيجة أو الخطأ. يمكن تسجيل الوظائف المستهلكة (الاشتراك فيها) باستخدام الطرق .then
و .catch
.
والأهم والأساسي هو .then
.
بناء الجملة هو:
الوعد.ثم( دالة (نتيجة) { /* التعامل مع نتيجة ناجحة */ }, الدالة (خطأ) { /* التعامل مع الخطأ */ } );
الوسيطة الأولى لـ .then
هي دالة يتم تشغيلها عند حل الوعد واستلام النتيجة.
الوسيطة الثانية لـ .then
هي دالة يتم تشغيلها عند رفض الوعد وتلقي الخطأ.
على سبيل المثال، إليك رد فعل على وعد تم حله بنجاح:
دع الوعد = وعد جديد (وظيفة (حل، رفض) { setTimeout(() => Resolve("done!"), 1000); }); // القرار يقوم بتشغيل الوظيفة الأولى في .then الوعد.ثم( النتيجة => تنبيه (نتيجة)، // يظهر "تم!" بعد 1 ثانية خطأ => تنبيه (خطأ) // لا يعمل );
تم تنفيذ الوظيفة الأولى.
وفي حالة الرفض الثاني:
دع الوعد = وعد جديد (وظيفة (حل، رفض) { setTimeout(() =>رفض(new Error("عفوا!")), 1000); }); // الرفض يقوم بتشغيل الوظيفة الثانية في .then الوعد.ثم( النتيجة => تنبيه (نتيجة)، // لا يعمل خطأ => تنبيه (خطأ) // يظهر "خطأ: عفوًا!" بعد 1 ثانية );
إذا كنا مهتمين فقط بالإكمال الناجح، فيمكننا توفير وسيطة دالة واحدة فقط لـ .then
:
دع الوعد = وعد جديد (الحل => { setTimeout(() => Resolve("done!"), 1000); }); وعد. ثم (تنبيه) ؛ // يظهر "تم!" بعد 1 ثانية
إذا كنا مهتمين فقط بالأخطاء، فيمكننا استخدام null
كوسيطة أولى: .then(null, errorHandlingFunction)
. أو يمكننا استخدام .catch(errorHandlingFunction)
، وهو نفسه تمامًا:
دع الوعد = وعد جديد ((يقرر، يرفض) => { setTimeout(() =>رفض(new Error("عفوا!")), 1000); }); // .catch(f) هو نفس الوعد. ثم(null, f) Promise.catch(alert); // يظهر "خطأ: عفوًا!" بعد 1 ثانية
النداء .catch(f)
هو تناظري كامل لـ .then(null, f)
، إنه مجرد اختصار.
تمامًا مثلما توجد جملة finally
في try {...} catch {...}
، هناك finally
الوعود.
النداء .finally(f)
يشبه .then(f, f)
بمعنى أن f
تجري دائمًا، عند قضاء الوعد: سواء أكان قرارًا أم رفضًا.
تتمثل فكرة finally
في إعداد معالج لإجراء التنظيف/الإنهاء بعد اكتمال العمليات السابقة.
على سبيل المثال، إيقاف مؤشرات التحميل، وإغلاق الاتصالات التي لم تعد هناك حاجة إليها، وما إلى ذلك.
فكر في الأمر باعتباره نهاية الحفلة. بغض النظر عما إذا كانت الحفلة جيدة أم سيئة، وعدد الأصدقاء الذين شاركوا فيها، فإننا لا نزال بحاجة (أو على الأقل يجب علينا) القيام بعملية تنظيف بعد انتهاء الحفلة.
قد يبدو الرمز كما يلي:
وعد جديد((حل، رفض) => { /* افعل شيئًا يستغرق وقتًا، ثم اتصل بالعزم أو ربما ارفضه */ }) // يعمل عند تسوية الوعد، لا يهم بنجاح أم لا .أخيرًا(() => مؤشر إيقاف التحميل) // لذلك يتوقف مؤشر التحميل دائمًا قبل المتابعة .ثم (النتيجة => إظهار النتيجة، خطأ => إظهار الخطأ)
يرجى ملاحظة أن finally(f)
ليس اسمًا مستعارًا لـ then(f,f)
تمامًا.
هناك اختلافات مهمة:
معالج finally
ليس لديه أي حجج. وفي finally
لا نعلم هل الوعد ناجح أم لا. هذا جيد، حيث أن مهمتنا عادة هي تنفيذ إجراءات الإنهاء "العامة".
يرجى إلقاء نظرة على المثال أعلاه: كما ترون، لا يحتوي المعالج finally
على أي وسيطات، ويتم التعامل مع نتيجة الوعد بواسطة المعالج التالي.
يقوم المعالج finally
"بتمرير" النتيجة أو الخطأ إلى المعالج المناسب التالي.
على سبيل المثال، هنا يتم تمرير النتيجة finally
إلى then
:
وعد جديد((حل، رفض) => { setTimeout(() => Resolve("value"), 2000); }) .finally(() => تنبيه("الوعد جاهز")) // يتم تشغيله أولاً .then(result => تنبيه(نتيجة)); // <-- .ثم يظهر "القيمة"
كما ترى، فإن value
التي أرجعها الوعد الأول يتم تمريرها finally
إلى then
التالي.
هذا مريح للغاية، لأنه finally
ليس المقصود منه معالجة نتيجة الوعد. كما قيل، إنه مكان لإجراء التنظيف العام، بغض النظر عن النتيجة.
وإليك مثال على الخطأ، لنرى كيف تم تمريره finally
catch
:
وعد جديد((حل، رفض) => { رمي خطأ جديد("خطأ"); }) .finally(() => تنبيه("الوعد جاهز")) // يتم تشغيله أولاً .catch(err => تنبيه(يخطئ)); // <-- يُظهر .catch الخطأ
لا ينبغي للمعالج finally
أيضًا إرجاع أي شيء. إذا حدث ذلك، فسيتم تجاهل القيمة التي تم إرجاعها بصمت.
الاستثناء الوحيد لهذه القاعدة هو عندما يقوم المعالج finally
بإلقاء خطأ. ثم ينتقل هذا الخطأ إلى المعالج التالي، بدلاً من أي نتيجة سابقة.
لتلخيص:
المعالج finally
لا يحصل على نتائج المعالج السابق (لا يحتوي على وسيطات). يتم تمرير هذه النتيجة بدلاً من ذلك إلى المعالج المناسب التالي.
إذا قام المعالج finally
بإرجاع شيء ما، فسيتم تجاهله.
عندما يلقي خطأ finally
، ينتقل التنفيذ إلى أقرب معالج خطأ.
هذه الميزات مفيدة وتجعل الأمور تعمل بالطريقة الصحيحة إذا استخدمنا finally
الطريقة التي من المفترض أن يتم استخدامها بها: لإجراءات التنظيف العامة.
يمكننا إرفاق المتعاملين بالوعود المستقرة
إذا كان هناك وعد معلق، فإن معالجات .then/catch/finally
تنتظر نتائجه.
في بعض الأحيان، قد يكون الوعد قد تمت تسويته بالفعل عندما نضيف معالجًا إليه.
في مثل هذه الحالة، تعمل هذه المعالجات على الفور:
// يُحل الوعد فورًا عند إنشائه دع الوعد = وعد جديد(resolve => Resolve("done!")); وعد. ثم (تنبيه) ؛ // منتهي! (يظهر الآن)
لاحظ أن هذا يجعل الوعود أقوى من سيناريو "قائمة الاشتراك" الواقعية. إذا كان المغني قد أصدر أغنيته بالفعل ثم قام شخص ما بالتسجيل في قائمة الاشتراك، فمن المحتمل ألا يتلقى تلك الأغنية. يجب أن تتم الاشتراكات في الحياة الواقعية قبل الحدث.
الوعود أكثر مرونة. يمكننا إضافة معالجات في أي وقت: إذا كانت النتيجة موجودة بالفعل، فسيتم تنفيذها فقط.
بعد ذلك، دعونا نرى المزيد من الأمثلة العملية حول كيف يمكن أن تساعدنا الوعود في كتابة تعليمات برمجية غير متزامنة.
لدينا وظيفة loadScript
لتحميل البرنامج النصي من الفصل السابق.
إليك المتغير القائم على رد الاتصال، فقط لتذكيرنا به:
وظيفة تحميل سكريبت (src، رد الاتصال) { دع البرنامج النصي = document.createElement('script'); script.src = src; script.onload = () => رد الاتصال(null, script); script.onerror = () => callback(new Error(`خطأ في تحميل البرنامج النصي لـ ${src}`)); document.head.append(script); }
دعونا نعيد كتابتها باستخدام الوعود.
لن تتطلب الوظيفة الجديدة loadScript
رد اتصال. بدلاً من ذلك، سيقوم بإنشاء وإرجاع كائن Promise الذي يتم حله عند اكتمال التحميل. يمكن للكود الخارجي إضافة معالجات (وظائف الاشتراك) إليه باستخدام .then
:
وظيفة تحميل سكريبت (سرك) { إرجاع وعد جديد (وظيفة (حل، رفض) { دع البرنامج النصي = document.createElement('script'); script.src = src; script.onload = () => حل(script); script.onerror = () => Deject(new Error(`خطأ في تحميل البرنامج النصي لـ ${src}`)); document.head.append(script); }); }
الاستخدام:
دع الوعد = LoadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); الوعد.ثم( script => تنبيه(`تم تحميل${script.src}!`)، خطأ => تنبيه (`خطأ: ${error.message}`) ); وعد. ثم(script => تنبيه('معالج آخر...'));
يمكننا أن نرى على الفور بعض الفوائد عبر النمط القائم على رد الاتصال:
وعود | عمليات الاسترجاعات |
---|---|
الوعود تسمح لنا بفعل الأشياء بالترتيب الطبيعي. أولاً، نقوم بتشغيل loadScript(script) ، .then نكتب ما يجب فعله بالنتيجة. | يجب أن تكون لدينا وظيفة callback تحت تصرفنا عند استدعاء loadScript(script, callback) . بمعنى آخر، يجب أن نعرف ما يجب فعله بالنتيجة قبل استدعاء loadScript . |
يمكننا الاتصال .then بناءً على الوعد عدة مرات كما نريد. في كل مرة، نقوم بإضافة "معجب" جديد، ووظيفة اشتراك جديدة، إلى "قائمة الاشتراك". المزيد عن هذا في الفصل التالي: تسلسل الوعود. | يمكن أن يكون هناك رد اتصال واحد فقط. |
لذا فإن الوعود تمنحنا تدفقًا ومرونة أفضل للتعليمات البرمجية. ولكن هناك المزيد. وسنرى ذلك في الفصول القادمة.
ما هو ناتج الكود أدناه؟
دع الوعد = وعد جديد (وظيفة (حل، رفض) { العزم(1); setTimeout(() => Resolve(2), 1000); }); وعد. ثم (تنبيه) ؛
الناتج هو : 1
.
يتم تجاهل الاستدعاء الثاني resolve
، لأنه يتم أخذ الاستدعاء الأول reject/resolve
فقط في الاعتبار. يتم تجاهل المكالمات الإضافية.
تستخدم الوظيفة المضمنة setTimeout
عمليات الاسترجاعات. إنشاء بديل قائم على الوعد.
يجب أن يُرجع delay(ms)
وعدًا. يجب أن يحل هذا الوعد بعد مللي ms
، حتى نتمكن من إضافة .then
إليه، على النحو التالي:
تأخير الوظيفة (مللي ثانية) { // الكود الخاص بك } تأخير (3000).then(() => تنبيه('يعمل بعد 3 ثوان'));
تأخير الوظيفة (مللي ثانية) { إرجاع وعد جديد (resolve => setTimeout(resolve, ms)); } تأخير (3000).then(() => تنبيه('يعمل بعد 3 ثوان'));
يرجى ملاحظة أنه في هذه المهمة يتم استدعاء resolve
بدون وسيطات. لا نقوم بإرجاع أي قيمة من delay
، فقط تأكد من التأخير.
أعد كتابة وظيفة showCircle
في حل المهمة الدائرة المتحركة مع رد الاتصال بحيث تُرجع وعدًا بدلاً من قبول رد الاتصال.
الاستخدام الجديد:
showCircle(150, 150, 100).ثم(div => { div.classList.add('message-ball'); div.append("مرحبا أيها العالم!"); });
خذ حل المهمة الدائرة المتحركة مع رد الاتصال كقاعدة.
افتح الحل في رمل.