دعنا نعود إلى المشكلة المذكورة في فصل المقدمة: عمليات الاسترجاعات: لدينا سلسلة من المهام غير المتزامنة التي يتعين علينا تنفيذها واحدة تلو الأخرى - على سبيل المثال، تحميل البرامج النصية. كيف يمكننا ترميزها بشكل جيد؟
توفر الوعود بعض الوصفات للقيام بذلك.
في هذا الفصل نغطي تسلسل الوعد.
يبدو مثل هذا:
وعد جديد (وظيفة (حل، رفض) { setTimeout(() => القرار(1), 1000); // (*) }).then(function(result) { // (**) تنبيه (نتيجة)؛ // 1 نتيجة الإرجاع * 2؛ }).then(function(result) { // (***) تنبيه (نتيجة)؛ // 2 نتيجة الإرجاع * 2؛ }).ثم(وظيفة(نتيجة) { تنبيه (نتيجة)؛ // 4 نتيجة الإرجاع * 2؛ });
والفكرة هي أن يتم تمرير النتيجة من خلال سلسلة معالجات .then
.
هنا التدفق هو:
يتم حل الوعد الأولي خلال ثانية واحدة (*)
,
ثم يتم استدعاء المعالج .then
(**)
، والذي بدوره ينشئ وعدًا جديدًا (يتم حله بقيمة 2
).
التالي then
(***)
يحصل على نتيجة السابق، ويعالجها (يضاعفها) ويمررها إلى المعالج التالي.
…وهلم جرا.
عندما يتم تمرير النتيجة عبر سلسلة المعالجات، يمكننا رؤية سلسلة من استدعاءات alert
: 1
→ 2
→ 4
.
الأمر برمته يعمل، لأن كل استدعاء لـ .then
يُرجع وعدًا جديدًا، حتى نتمكن من استدعاء .then
التالي عليه.
عندما يقوم المعالج بإرجاع قيمة، فإنها تصبح نتيجة ذلك الوعد، لذلك يتم استدعاء .then
التالي معها.
خطأ كلاسيكي للمبتدئين: من الناحية الفنية، يمكننا أيضًا إضافة العديد .then
إلى وعد واحد. هذه ليست تسلسل.
على سبيل المثال:
دع الوعد = وعد جديد (وظيفة (حل، رفض) { setTimeout(() => القرار(1), 1000); }); وعد. ثم (وظيفة (نتيجة) { تنبيه (نتيجة)؛ // 1 نتيجة الإرجاع * 2؛ }); وعد. ثم (وظيفة (نتيجة) { تنبيه (نتيجة)؛ // 1 نتيجة الإرجاع * 2؛ }); وعد. ثم (وظيفة (نتيجة) { تنبيه (نتيجة)؛ // 1 نتيجة الإرجاع * 2؛ });
ما فعلناه هنا هو مجرد إضافة عدة معالجات لوعد واحد. إنهم لا يمررون النتيجة لبعضهم البعض؛ بدلاً من ذلك يقومون بمعالجتها بشكل مستقل.
هذه هي الصورة (قارنها بالتسلسل أعلاه):
كل ذلك .then
على نفس الوعد يحصلون على نفس النتيجة – نتيجة ذلك الوعد. لذلك في الكود أعلاه كل alert
يظهر نفس الشيء: 1
.
من الناحية العملية، نادرًا ما نحتاج إلى معالجات متعددة لوعد واحد. يتم استخدام التسلسل في كثير من الأحيان.
قد يقوم المعالج المستخدم في .then(handler)
بإنشاء وعد وإرجاعه.
في هذه الحالة، تنتظر معالجات أخرى حتى تستقر، ثم تحصل على نتيجتها.
على سبيل المثال:
وعد جديد (وظيفة (حل، رفض) { setTimeout(() => القرار(1), 1000); }).ثم(وظيفة(نتيجة) { تنبيه (نتيجة)؛ // 1 إرجاع وعد جديد ((حل، رفض) => { // (*) setTimeout(() => Resolve(result * 2), 1000); }); }).then(function(result) { // (**) تنبيه (نتيجة)؛ // 2 إرجاع وعد جديد ((حل، رفض) => { setTimeout(() => Resolve(result * 2), 1000); }); }).ثم(وظيفة(نتيجة) { تنبيه (نتيجة)؛ // 4 });
هنا يعرض .then
1
ويعيد new Promise(…)
في السطر (*)
. بعد ثانية واحدة يتم حلها، ويتم تمرير النتيجة (وسيطة resolve
، هنا result * 2
) إلى معالج الثانية .then
. هذا المعالج موجود في السطر (**)
ويظهر 2
ويفعل نفس الشيء.
وبالتالي فإن الإخراج هو نفسه كما في المثال السابق: 1 → 2 → 4، ولكن الآن مع تأخير لمدة ثانية واحدة بين مكالمات alert
.
يتيح لنا إرجاع الوعود بناء سلاسل من الإجراءات غير المتزامنة.
دعونا نستخدم هذه الميزة مع loadScript
الموعود، الذي تم تعريفه في الفصل السابق، لتحميل البرامج النصية واحدًا تلو الآخر، بالتسلسل:
LoadScript("https://javascript.info/article/promise-chaining/one.js") .ثم (وظيفة (البرنامج النصي) { return LoadScript("https://javascript.info/article/promise-chaining/two.js"); }) .ثم (وظيفة (البرنامج النصي) { return LoadScript("https://javascript.info/article/promise-chaining/three.js"); }) .ثم (وظيفة (البرنامج النصي) { // استخدم الوظائف المعلنة في البرامج النصية // لإظهار أنه تم تحميلها بالفعل واحد()؛ اثنين()؛ ثلاثة()؛ });
يمكن جعل هذا الرمز أقصر قليلاً باستخدام وظائف السهم:
LoadScript("https://javascript.info/article/promise-chaining/one.js") .then(script =>loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script =>loadScript("https://javascript.info/article/promise-chaining/three.js")) .ثم (النص => { // تم تحميل البرامج النصية، يمكننا استخدام الوظائف المعلنة هناك واحد()؛ اثنين()؛ ثلاثة()؛ });
هنا يُرجع كل استدعاء loadScript
وعدًا، ويتم تشغيل .then
التالي عندما يتم حله. ثم يبدأ تحميل البرنامج النصي التالي. لذلك يتم تحميل البرامج النصية واحدا تلو الآخر.
يمكننا إضافة المزيد من الإجراءات غير المتزامنة إلى السلسلة. يرجى ملاحظة أن الكود لا يزال "مسطحًا" - فهو ينمو للأسفل، وليس إلى اليمين. لا توجد علامات على "هرم الهلاك".
من الناحية الفنية، يمكننا إضافة .then
مباشرة إلى كل loadScript
، مثل هذا:
LoadScript("https://javascript.info/article/promise-chaining/one.js").then(script1 => { LoadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { LoadScript("https://javascript.info/article/promise-chaining/three.js").then(script3 => { // هذه الوظيفة لديها حق الوصول إلى المتغيرات script1 وscript2 وscript3 واحد()؛ اثنين()؛ ثلاثة()؛ }); }); });
يفعل هذا الرمز نفس الشيء: يقوم بتحميل 3 نصوص برمجية بالتسلسل. لكنه "ينمو إلى اليمين". لذلك لدينا نفس المشكلة كما هو الحال مع عمليات الاسترجاعات.
الأشخاص الذين يبدأون في استخدام الوعود أحيانًا لا يعرفون شيئًا عن التسلسل، لذلك يكتبونها بهذه الطريقة. بشكل عام، يفضل استخدام التسلسل.
في بعض الأحيان يكون من المقبول كتابة .then
مباشرة، لأن الوظيفة المتداخلة لديها حق الوصول إلى النطاق الخارجي. في المثال أعلاه، رد الاتصال الأكثر تداخلا لديه حق الوصول إلى كافة المتغيرات script1
، script2
، script3
. لكن هذا استثناء وليس قاعدة.
ثمابلز
على وجه الدقة، قد لا يُرجع المعالج وعدًا تمامًا، بل ما يسمى بالكائن "القابل للتنفيذ" - وهو كائن عشوائي له طريقة .then
. سيتم التعامل معه بنفس طريقة الوعد.
والفكرة هي أن مكتبات الطرف الثالث قد تنفذ كائنات "متوافقة مع الوعد" خاصة بها. يمكن أن يكون لديهم مجموعة موسعة من الأساليب، ولكنهم أيضًا يكونون متوافقين مع الوعود الأصلية، لأنهم ينفذون .then
.
فيما يلي مثال على كائن يمكن استخدامه:
فئة ثمابل { منشئ (عدد) { this.num = num; } ثم (حل، رفض) { تنبيه (العزم)؛ // الوظيفة () { الكود الأصلي } // قم بالحل باستخدام this.num*2 بعد ثانية واحدة setTimeout(() => Resolve(this.num * 2), 1000); // (**) } } وعد جديد (الحل => العزم (1)) .ثم(النتيجة => { إرجاع ثينابلي جديد (نتيجة) ؛ // (*) }) .ثم(تنبيه); // يظهر 2 بعد 1000 مللي ثانية
تتحقق JavaScript من الكائن الذي تم إرجاعه بواسطة معالج .then
في السطر (*)
: إذا كان يحتوي على طريقة قابلة للاستدعاء تسمى then
، فإنها تستدعي تلك الطريقة التي توفر وظائف أصلية resolve
، reject
كوسائط (على غرار المنفذ) وتنتظر حتى يتم حل أحدهما يسمى. في المثال أعلاه، يتم استدعاء resolve(2)
بعد ثانية واحدة (**)
. ثم يتم تمرير النتيجة إلى أسفل السلسلة.
تتيح لنا هذه الميزة دمج الكائنات المخصصة مع سلاسل الوعد دون الحاجة إلى الوراثة من Promise
.
في برمجة الواجهة الأمامية، غالبًا ما تُستخدم الوعود لطلبات الشبكة. لذلك دعونا نرى مثالا موسعا على ذلك.
سنستخدم طريقة الجلب لتحميل المعلومات الخاصة بالمستخدم من الخادم البعيد. يحتوي على الكثير من المعلمات الاختيارية المغطاة في فصول منفصلة، ولكن بناء الجملة الأساسي بسيط للغاية:
دع الوعد = fetch(url);
يؤدي هذا إلى إرسال طلب شبكة إلى url
وإرجاع وعد. يتم حل الوعد باستخدام كائن response
عندما يستجيب الخادم البعيد بالترويسات، ولكن قبل تنزيل الاستجابة الكاملة .
لقراءة الرد الكامل، يجب أن نستدعي التابع response.text()
: فهو يعيد وعدًا يتم حله عند تنزيل النص الكامل من الخادم البعيد، مع هذا النص نتيجة لذلك.
يقوم الكود أدناه بتقديم طلب إلى user.json
ويقوم بتحميل النص الخاص به من الخادم:
جلب('https://javascript.info/article/promise-chaining/user.json') // .ثم يتم تشغيله أدناه عندما يستجيب الخادم البعيد .ثم (وظيفة (الاستجابة) { يُرجع // Response.text() وعدًا جديدًا يتم حله بنص الاستجابة الكامل // عند التحميل إرجاع الاستجابة.نص ()؛ }) .ثم (وظيفة (نص) { // ... وإليك محتوى الملف البعيد تنبيه(نص); // {"name": "iliakan"، "isAdmin": صحيح} });
يتضمن كائن response
الذي تم إرجاعه من fetch
أيضًا الطريقة response.json()
التي تقرأ البيانات البعيدة وتحللها على أنها JSON. في حالتنا، هذا أكثر ملاءمة، لذلك دعونا ننتقل إليه.
سنستخدم أيضًا وظائف الأسهم للإيجاز:
// كما هو مذكور أعلاه، لكن Response.json() يوزع المحتوى البعيد كـ JSON جلب('https://javascript.info/article/promise-chaining/user.json') .ثم(الاستجابة => الاستجابة.json()) .then(user => تنبيه(user.name)); // إليكان، حصلت على اسم المستخدم
الآن دعونا نفعل شيئًا مع المستخدم الذي تم تحميله.
على سبيل المثال، يمكننا تقديم طلب آخر إلى GitHub، وتحميل ملف تعريف المستخدم وإظهار الصورة الرمزية:
// قم بتقديم طلب لـ user.json جلب('https://javascript.info/article/promise-chaining/user.json') // قم بتحميله كـ json .ثم(الاستجابة => الاستجابة.json()) // قدم طلبًا إلى GitHub .then(user => fetch(`https://api.github.com/users/${user.name}`)) // قم بتحميل الاستجابة بتنسيق json .ثم(الاستجابة => الاستجابة.json()) // إظهار الصورة الرمزية (githubUser.avatar_url) لمدة 3 ثوانٍ (ربما تحريكها) .ثم(githubUser => { Let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "مثال للصورة الرمزية للوعد"; document.body.append(img); setTimeout(() => img.remove(), 3000); // (*) });
الكود يعمل انظر التعليقات حول التفاصيل. ومع ذلك، هناك مشكلة محتملة فيه، وهو خطأ نموذجي لأولئك الذين يبدأون في استخدام الوعود.
انظر إلى السطر (*)
: كيف يمكننا أن نفعل شيئًا بعد انتهاء الصورة الرمزية من الظهور وإزالتها؟ على سبيل المثال، نود أن نعرض نموذجًا لتحرير هذا المستخدم أو أي شيء آخر. حتى الآن، لا توجد طريقة.
لجعل السلسلة قابلة للتمديد، نحتاج إلى إرجاع وعد يتم حله عندما تنتهي الصورة الرمزية من الظهور.
مثله:
جلب('https://javascript.info/article/promise-chaining/user.json') .ثم(الاستجابة => الاستجابة.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .ثم(الاستجابة => الاستجابة.json()) .then(githubUser => new Promise(function(resolve,reject) { // (*) Let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "مثال للصورة الرمزية للوعد"; document.body.append(img); setTimeout(() => { img.remove(); حل(githubUser); // (**) }, 3000); })) // يتم تشغيله بعد 3 ثوانٍ .then(githubUser => تنبيه(`انتهى عرض ${githubUser.name}`));
وهذا يعني أن معالج .then
في السطر (*)
يُرجع الآن new Promise
، والذي لا تتم تسويته إلا بعد استدعاء resolve(githubUser)
في setTimeout
(**)
. التالي .then
في السلسلة سوف ينتظر ذلك.
كممارسة جيدة، يجب أن يؤدي الإجراء غير المتزامن دائمًا إلى إرجاع الوعد. وهذا يجعل من الممكن التخطيط للإجراءات بعد ذلك؛ وحتى لو لم نخطط لتمديد السلسلة الآن، فقد نحتاج إليها لاحقًا.
أخيرًا، يمكننا تقسيم الكود إلى وظائف قابلة لإعادة الاستخدام:
وظيفة تحميلJson(url) { جلب العودة (URL) .then(response => Response.json()); } وظيفة تحميلGithubUser(الاسم) { إرجاع LoadJson(`https://api.github.com/users/${name}`); } وظيفة showAvatar(githubUser) { إرجاع وعد جديد (وظيفة (حل، رفض) { Let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "مثال للصورة الرمزية للوعد"; document.body.append(img); setTimeout(() => { img.remove(); حل(githubUser); }, 3000); }); } // استخدمهم: LoadJson('https://javascript.info/article/promise-chaining/user.json') .then(user =>loadGithubUser(user.name)) .ثم (إظهار الصورة الرمزية) .then(githubUser => تنبيه(`انتهى عرض ${githubUser.name}`)); // ...
إذا قام المعالج .then
(أو catch/finally
، لا يهم) بإرجاع وعد، فإن بقية السلسلة تنتظر حتى تستقر. عندما يحدث ذلك، يتم تمرير النتيجة (أو الخطأ) إلى أبعد من ذلك.
وهنا صورة كاملة:
هل أجزاء التعليمات البرمجية هذه متساوية؟ بمعنى آخر، هل يتصرفون بنفس الطريقة في أي ظرف من الظروف، بالنسبة لأي وظائف معالج؟
وعد. ثم (f1).catch(f2);
عكس:
وعد. ثم (f1، f2)؛
الإجابة المختصرة هي: لا، ليسا متساويين :
الفرق هو أنه إذا حدث خطأ في f1
، فسيتم معالجته بواسطة .catch
هنا:
يعد .ثم(f1) .catch(f2);
…ولكن ليس هنا:
يعد .ثم(f1, f2);
وذلك لأنه تم تمرير خطأ إلى أسفل السلسلة، وفي الجزء الثاني من الكود لا توجد سلسلة أقل من f1
.
بمعنى آخر، .then
يمرر النتائج/الأخطاء إلى .then/catch
التالي. لذلك، في المثال الأول، يوجد catch
أدناه، وفي المثال الثاني لا يوجد، لذلك لم تتم معالجة الخطأ.