اذهب إلى المحتوى

لوحة المتصدرين

  1. أسامة زيادة

    أسامة زيادة

    الأعضاء


    • نقاط

      2

    • المساهمات

      2501


  2. Bandar Abuseada

    Bandar Abuseada

    الأعضاء


    • نقاط

      2

    • المساهمات

      250


  3. Motzashi

    Motzashi

    الأعضاء


    • نقاط

      2

    • المساهمات

      2


  4. عمر قره محمد

    عمر قره محمد

    الأعضاء


    • نقاط

      1

    • المساهمات

      4096


المحتوى الأكثر حصولًا على سمعة جيدة

المحتوى الأعلى تقييمًا في 07/16/23 في كل الموقع

  1. السلام عليكم هل يجب تعلم طريقة كتابة اكواد api JSON وفهم get post او نكتفي بالمفهوم العام ؟ انا ماشي في كورس ومقطع ال api فهمت وظيفته لكن بالتعمق كان متعب وصعب فهمه
    2 نقاط
  2. السلام عليكم احبتي عندما احرك عجله الموس الضغط على أسهم الكيبورد علا وأسفل يختفي عناوين الصفحة كيف العمل كي يظل العنوان ثابت
    1 نقطة
  3. ما هي وظائف كل من 1- useCallback 2- useMemo * بالتفصيل الممل وأمثله من مواقع حقيقة عليهما
    1 نقطة
  4. نعم، يمكنك استخدام "useEffect" بدلاً من "useCallback" في بعض الحالات، ولكن هناك بعض الاختلافات في الوظائف والأغراض التي يخدمانها. useCallback يستخدم "useCallback" لتذكر دالة معينة وتحديد الاعتماديات (dependencies) التي إذا تغيرت، سيتم إعادة إنشاء الدالة. ينصح بشدة باستخدام "useCallback" عند تمرير دالة كخاصية (prop) لمكون فرعي (sub-component)، حيث يساعد في تجنب إعادة إنشاء الدالة في كل تحديث. useEffect يستخدم "useEffect" للتعامل مع العمليات أو التأثيرات الجانبية (side effects) في المكون، مثل استدعاء الواجهات البرمجية الخارجية أو إضافة مستمعين للأحداث أو التلاعب بالمكونات الأخرى. يتم تنفيذ "useEffect" في كل مرة يتم فيها إعادة رسم المكون، مما يعني أنه يمكن أن يحدث في كل تحديث للمكون. على الرغم من أنه يمكن استخدام "useEffect" لتحقيق نفس الغرض العام لـ "useCallback"، إلا أن هناك فارقاً هاماً بينهما. عند استخدام "useEffect"، سيتم إعادة تنفيذ الكود المحتوى فيه في كل تحديث للمكون، بينما يتم إعادة تنفيذ الدالة المذكورة في "useCallback" فقط عند تغير الاعتماديات. إذا كنت تحتاج فقط لتحقيق التأثير الجانبي مرة واحدة عند تغير الاعتماديات، فإن استخدام "useCallback" هو الخيار المناسب. إذا كنت تحتاج إلى تنفيذ تأثير جانبي في كل تحديث للمكون، بغض النظر عن التغير في الاعتماديات، فيمكنك استخدام "useEffect". تذكر أنه في بعض الحالات قد تحتاج إلى استخدام كلا الهوكس معًا لتحقيق التأثير المطلوب، وذلك حسب احتياجات تطبيقك الخاص.
    1 نقطة
  5. في ميزة أعجبتني في موقع في سلة.. ببساطة هي أقسام المتجر مصصمة بشكل أيقونات تفاعلية. هل اقدر انسخ اكواد ال css لهذه الخاصية وأضيفها في متجري؟
    1 نقطة
  6. كل من useCallback و useMemo هما هوكز (Hooks) في مكتبة React لإدارة الحالة والأداء في تطبيقات React. يتم استخدام هذين الهوك لتحسين أداء التطبيق عن طريق تقليل العمل الزائد وإعادة العمليات غير الضرورية. useCallback useCallback هو هوك يسمح بتذكر واستخدام دالة معينة بصورة مرنة. يستخدم عادة عندما تحتاج إلى تمرير دالة إلى مكون فرعي كخاصية (prop)، وترغب في تجنب إعادة إنشاء الدالة في كل مرة يتم فيها إعادة رسم المكون الأصلي. بوجود useCallback، يمكنك ضمان أن الدالة لا تعاد إلا عندما تتغير إحدى الاعتماديات (dependencies) التي تمرّرها إليه. مثال import React, { useState, useCallback } from 'react'; function MyComponent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { setCount(count + 1); }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> </div> ); } في المثال أعلاه، يتم استخدام useCallback لتذكر الدالة handleClick وتمريرها إلى خاصية onClick للزر. يتم إعادة إنشاء الدالة handleClick فقط عندما يتغير count. useMemo useMemo هو هوك يسمح بحفظ نتيجة عملية محسوبة وإعادة استخدامها عندما تكون الاعتماديات ثابتة. يستخدم عادة لتجنب إعادة حساب قيم مكلفة مثل العمليات الحسابية أو تحويلات البيانات في كل مرة يتم فيها إعادة رسم المكون. مثال: import React, { useMemo } from 'react'; function MyComponent({ items }) { const totalPrice = useMemo(() => { return items.reduce((sum, item) => sum + item.price, 0); }, [items]); return ( <div> <p>Total Price: {totalPrice}</p> </div> ); } في المثال أعلاه، يتم استخدام useMemo لحساب totalPrice بناءً على items الممررة كاعتمادية. يتم حساب totalPrice فقط عندما يتغير items، وإلا سيتم إعادة استخدام القيمة المحسوبة السابقة. باستخدام useCallback و useMemo، يمكن تحسين أداء تطبيقات React وتقليل العمل الزائد وإعادة العمليات غير الضرورية.
    1 نقطة
  7. ما هم و ما وضيفتهم useState Hook useEffect Hook useRef Hook useCallback Hook useMemo Hook useContext Hook useReducer Hook
    1 نقطة
  8. ()forEeach(), Map(), Filter() And Reduce ما هم و ما الفرق بينهم و ما الفرق بيمنهم و بين for loob و while loob لاهنم ارى انهم نفس شئ الذي ينتج عن ذلك حيره اي منهم استعمل في مشروعي مثلا؟
    1 نقطة
  9. كل من هذه ال hooks يحتاج إلى مقال منفصل حتى يتم الحديث عنه ولذلك سأخبرك لمحة عن كل منها وعليك الاطلاع على المقالات التي سأرشحها لك لتتعرف اكثر عن كل منها : useState : يسمح لك هذا بإضافة حالة (state) إلى المكون الخاص بك. حيث يعيد زوجًا من القيم: الحالة الحالية ودالة التحديث. ونستخدم هذا الخطاف لمعالجة البيانات التي نريد ان نحدث المكون عند تغيرها (وذلك لأنها تسبب اعادة بناء المكون عند كل تحديث). useEffect : يتيح لك هذا مزامنة المكون (component) ليقوم بالتحديث عند تغير قيمة معينة. وهو مشابه لطرق دورة حياة المكونات (componentDidMount و componentDidUpdate و componentWillUnmount). ومن اشهر استخداماته هو التحقق من كون المستخدم قد قام بتسجيل الدخول قبل ان نقوم بعرض الصفحة. useRef : يتيح لك هذا الإشارة إلى قيمة لا نريد تحديث المكون عند تغير قيمتها.حيث يعيد كائنًا قابلًا للتغير يحتوي على خاصية .current التي تم تهيئتها بالمعامل الممرر (initialValue). واشهر استخداماته هو التحكم في عناصر الـ html باستخدام js. useCallback هو خطاف يعيد نسخة مخزنة من الدالة التي تمرر إليه. يتم تحديث هذه النسخة المخزنة فقط عندما يتغير أحد التبعيات المحددة. يمكن استخدام useCallback لتحسين الأداء في حالات معينة. useMemo : يتيح لك هذا تخزين نتائج معينة بشكل لا يتأثر بتحديثات المكون. ويستخدم بشكل اساسي لتحسين الاداء في التطبيقات المعقدة. useContext : يتيح لك هذا الاستخدام القيمة الحالية للسياق (context). حيث يعيد القيمة الحالية للسياق المعطى. حيث يكون قد تم إنشاء سياق في المكون الاساسي للتطبيق، واشيع استخدام له هو الوضع الليلي والنهاري للموقع. useReducer : يتيح لك هذا استخدام مخزن Redux في المكونات (components) الوظيفية. حيث يعيد زوجًا من القيم: الحالة (state) الحالية ودالة التحديث. ويجب ان تتعرف على Redux حتى تستطيع العمل معه بشكل جيد. مراجع مهمة : الخطافات من موسوعة حسوب مرجع إلى الواجهة البرمجية للخطافات في React المقالات المتعلقة بـ react
    1 نقطة
  10. الفرق هو أن auth()->user() يرجع الكائن User الحالي بينما Auth::user() يرجع الكائن User الحالي أو null إذا لم يكن هناك مستخدم محدد. وتستطيع استخدام auth()->user() في المواقف التي تحتاج فيها إلى التأكد من وجود مستخدم محدد، مثلاً في حال كنت تريد عرض معلومات المستخدم الحالي ، فاستخدم: auth()->user()->name بينما نستخدم Auth::user() في المواقف التي لا تحتاج فيها إلى التأكد من وجود مستخدم محدد. كمثل معرفة ما إذا كان هناك مستخدم محدد متصل. وإليك مثال: // استخدام auth()->user() $user = auth()->user(); // طباعة اسم المستخدم الحالي echo $user->name; // استخدام Auth::user() if (Auth::check()) { // هناك مستخدم متصل } else { // لا يوجد مستخدم متصل }
    1 نقطة
  11. في Vue 3، هناك تغييرات في كيفية تعريف واستخدام Custom Directives بالمقارنة مع Vue 2. في Vue 3، تم تعديل الطريقة التي يتم بها استدعاء الدالة المستخدمة لتعريف الـ Custom Directive. في ملف directives.js، يمكنك تعديل التصدير بالشكل التالي: // directives.js import { app } from './main' export default function setupDirectives() { app.directive('globalexternal', { mounted: function (el) { el.placeholder = 'Global Custom Directive from external file' } }) console.log('Global Custom Directive from external file') } ثم في ملف main.js، يمكنك تعديل الاستيراد وإنشاء التطبيق بالشكل التالي: // main.js import { createApp } from 'vue' import App from './App.vue' export const app = createApp(App) import setupDirectives from './directives' setupDirectives() app.mount('#app') تأكد من استيراد app و setupDirectives في الملف main.js واستدعاء setupDirectives() قبل استدعاء app.mount('#app'). هذا يضمن تعريف الـ Custom Directive قبل تجميع التطبيق. بعد هذه التعديلات، يجب أن يعمل <input v-globalexternal style="width: 370px" /> كما هو متوقع. يتم تعريف الـ Custom Directive في الملف directives.js ويتم تطبيقه في الملف App.vue باستخدام الـ v-globalexternal.
    1 نقطة
  12. معالج الحاسوب هو تلك الرقاقة المركزية بين دارات الحاسوب المنفِّذة للخطوات الصغيرة التي تتشكل منها البرامج، وما كانت البرامج التي رأيناها حتى الآن إلا أشياء تبقيه مشغولًا إلى أن تنهي مهامها، كما تعتمد سرعة تنفيذ تعليمة برمجية مثل حلقة تكرارية تعالج بيانات على سرعة المعالج نفسه. تتواجد مع هذا عدة برامج تتفاعل مع أشياء خارج المعالج، فقد تتواصل عبر شبكة حاسوبية، أو تطلب بيانات من قرص صلب مثلًا، والذي قد يكون أبطأ من الحصول على المعلومة نفسها من القرص الصلب، وحين يحدث مثل ذلك، فعلينا معرفة ما إذا كان يليق بنا ترك المعالج يقبع ساكنًا دون عمل، ولهذا لا بد من وجود عمل يمكنه تنفيذه في ذلك الوقت، كما يعالج نظام تشغيلك ذلك جزئيًا، حيث سيجعل المعالج يتنقل بين عدة برامج عاملة، لكن هذا لا ينفع حين نريد لبرنامج واحد العمل بينما ينتظر ردًا من الشبكة. عدم التزامن Asynchronicity تحدث الأشياء في نموذج البرمجة المتزامنة synchronous واحدةً تلو الأخرى، فلا تعيد دالة تنفِّذ إجراءً يعمل على المدى الطويل إلا إذا وقع الإجراء وصار بإمكانه إعادة النتيجة، وبالتالي سيُوقِف هذا برنامجك إلى حين تنفيذ ذلك الإجراء؛ أما النموذج غير المتزامن asynchronous فسيسمح بحدوث عدة أشياء في الوقت نفسه، حيث سيستمر برنامجك بالعمل إذا بدأت بإجراء ما، كما سيُبلَّغ البرنامج حين ينتهي الإجراء ويحصل على وصول access إلى النتيجة مثل قراءة البيانات من القرص. نستطيع موازنة البرمجة المتزامنة وغير المتزامنة باستخدام مثال بسيط، وهو برنامج يجلب مَوردَين من الشبكة ثم يوازن النتيجة، بحيث تُعيد دالة الطلب في البيئة المتزامنة بعد إنهاء عملها. وأسهل طريقة لتنفيذ ذلك الإجراء هي جعل الطلبات واحدًا تلو الآخر، وهذا له تبعته، إذ لن يبدأ الطلب الثاني إلا حين ينتهي الطلب الأول، كما سيكون الوقت الكلي المستغرق مساويًا وقت رد الطلبَين معًا. يكون حل تلك المشكلة في النظام المتزامن ببدء خيوط تحكم control threads إضافية، وقد يكون الخيط هنا برنامجًا آخرًا عاملًا قد يتزامن تنفيذه مع برامج أخرى بواسطة نظام التشغيل، وبما أنّ أغلب الحواسيب الحديثة تحتوي على عِدة معالجات، فقد تعمل الخيوط المتعددة في الوقت نفسه، بل قد تعمل على معالجات مختلفة، فيبدأ خيط جديد الطلب الثاني، ثم ينتظر الخَيطان عودة نتائجهما، وبعد ذلك يُعيدا المزامنة من أجل دمج النتائج. تمثِّل الخطوط السميكة thick lines في المخطط التالي الوقت الذي ينفقه البرنامج في العمل العادي، في حين تمثِّل الخطوط الرفيعة thin lines وقت انتظار الشبكة، كما يكون الوقت الذي تأخذه الشبكة في النموذج المتزامن جزءًا من الخط الزمني timeline لخط تحكم ما؛ أما في النموذج غير المتزامن، فيتسبب بدء إجراء شبكة في حدوث انقسام split في الخط الزمني، كما يستمر البرنامج الذي ابتدأ الحدث بالعمل، ويقع الحدث معه أيضًا، ثم ينبه البرنامج حين ينتهي. هناك طريقة أخرى تصف الفرق بأنّ انتظار انتهاء الأحداث في النموذج المتزامن أمر ضمني implicit، بينما يكون صريحًا explicit وتحت تحكمنا في النموذج غير المتزامن. تتقاطع اللاتزامنية مع كلا الطريقتين، فهي تسهل التعبير عن البرامج التي لا تتوافق مع النموذج الخطي للتحكم، في حين قد تصعِّب التعبير عن البرامج التي تتبع النموذج الخطي، وسنرى بعض الطرق للنظر في تلك الغرابة في هذا المقال لاحقًا. تجعل المنصَّتان الهامتان لجافاسكربت -أي المتصفحات وNode.js- العمليات التي تستغرق وقتًا غير متزامنة، بدلًا من الاعتماد على الخيوط، وذلك جيد بما أنّ البرمجة بالخيوط صعبة جدًا بسبب صعوبة فهم مهمة البرنامج حين ينفِّذ أمورًا كثيرةً في وقت واحد. تقنية الغراب تستطيع الغربان استخدام الأدوات والتخطيط وتذكر الأشياء، وإيصال هذه الأشياء فيما بينهم، إذ أن شأنها في ذلك شأن غيرها من المخلوقات. لكن ما لا يعلمه الكثير عنها أنها قادرة على أمور أكثر من ذلك، إذ تُنشئ العديد من أسراب الغربان أجهزةً حاسوبيةً حيويةً مثلًا، فهي ليست إلكترونية مثل التي نصنعها، لكنها تتكون من حشرات دقيقة أشبه بالنمل الأبيض، إذ تُطوِّر معها علاقةً تكافليةً بحيث تزودها الغربان بالطعام مقابل أن تبني الحشرات تلك المستعمرات المعقدة وتشغلها، لتُجري حسابات لاحقًا بمساعدة كائنات حية داخلها. تقع مثل تلك المستعمرات عادةً في أعشاش كبيرة، حيث تعمل الطيور والحشرات معًا فيها لبناء هياكل طينية بصلية الشكل مخفية داخل أغصان الأعشاش تعيش فيها الحشرات وتعمل، وتَستخدِم تلك الآلات إشارات ضوئيةً للتواصل مع الأجهزة الأخرى، إذ تُدخِل الغربان قطعًا عاكسةً للضوء في سيقان خاصة بالتواصل، ثم توجه الحشرات تلك السيقان لعكس الضوء للأعشاش الأخرى، وهذا يعني أنّ الأعشاش التي بينها مجال بصري مفتوح هي التي تستطيع التواصل فقط. رسم أحد الخبراء خارطةً لشبكة أعشاش الغربان في قرية إييغ-سوغ-امبي Hières-sur-Amby الفرنسية، بحيث توضِّح أماكن الأعشاش واتصالاتها، كما في الشكل التالي: سنكتب في هذا المقال بعض الدوال الشبكية البسيطة لمساعدة الغربان على التواصل. ردود النداء Callbacks يمكن النظر إلى البرمجة غير المتزامنة على أنها تجعل الدوال التي تنفِّذ إجراءً بطيئًا تأخذ وسيطًا إضافيًا يكون دالة رد نداء callback function، حيث يبدأ الإجراء، ثم تُستدعى دالة رد النداء بالنتيجة حين ينتهي. لدينا مثلًا دالة setTimeout المتاحة في المتصفحات وNode.js على سواء، إذ تنتظر زمنًا بعينه يقاس بالميلي ثانية، ثم تَستدعي الدالة. setTimeout(() => console.log("Tick"), 500); لا يُعَدالانتظار عملًا مهمًا، لكن قد يكون مفيدًا حين نفعل أمرًا مثل تحديث رسم تحريكي أو التحقق من استغراق شيء وقتًا أطول من الوقت المسموح به، ويعني تنفيذ عدة أحداث غير متزامنة متتالية باستخدام ردود النداء؛ أنه علينا الاستمرار بتمرير دوال جديدة لمعالجة استمرارية الحوسبة بعد الحدث. تحتوي أغلب حواسيب أعشاش الغربان على حوصلة bulb تخزين بيانات طويل الأمد، حيث تُحفَر أجزاء المعلومات في الأغصان كي تُستخدَم لاحقًا، ويستغرق ذلك الحفر أو إيجاد جزء من البيانات وقتًا، لذا فإن واجهة التخزين طويل الأمد غير متزامنة، كما تَستخدِم دوال رد نداء. تخزِّن حواصل التخزين أجزاءً من بيانات JSON القابلة للتشفير تحت أسماء، وقد يخزِّن الغراب معلومات عن المكان الذي فيه الطعام المخبأ تحت اسم "food caches"، والذي قد يحمل مصفوفةً من الأسماء التي تشير إلى أجزاء أخرى من البيانات التي تصف الذاكرة المؤقتة الحقيقية، كما سيشغل الغراب مثل الشيفرة التالية للبحث عن مخزون طعام في حواصل التخزين storage bulbs في عش "Big Oak": import {bigOak} from "./crow-tech"; bigOak.readStorage("food caches", caches => { let firstCache = caches[0]; bigOak.readStorage(firstCache, info => { console.log(info); }); }); يُعَدّ هذا النسق من البرمجة قابلًا للتطبيق، لكنه يزيد من مستوى الإزاحة مع كل حدث غير متزامن، وذلك لأننا سنصل إلى دالة أخرى تفعل أمورًا أعقد من التي بين أيدينا لا محالة، مثل تنفيذ أحداث متعددة في الوقت نفسه، إذ سيكون الأمر غير مألوف. تُبنى حواسيب الغربان العشيَّة لتتواصل فيما بينها باستخدام أزواج من طلب-رد request-response، وهذا يعني أنّ أحد الأعشاش سيرسل رسالةً إلى عش آخر، ثم يرد الثاني مباشرةً مؤكدًا استلام الرسالة، وربما يرد أيضًا على الرسالة في الوقت نفسه، كما تُوسَم كل رسالة بنوع يحدد كيفية معالجتها، إذ تستطيع شيفرتنا تعريف المعالِجات handlers لكل نوع من أنواع الطلبات، ويُستدعى المعالج متى ما دخل ذلك الطلب لإنتاج رد. توفِّر الواجهة التي صدَّرتها وحدة ‎"./crow-tech"‎ دوالًا مبنيةً على ردود النداء من أجل التواصل، كما تحتوي الأعشاش على التابع send الذي يرسل الطلب، إذ يتوقع اسم العش الهدف ونوع الطلب ومحتواه على أساس أول ثلاثة وسائط، ثم يتوقع دالةً لتُستدعى حين يأتي الرد على أساس وسيط رابع وأخير. bigOak.send("Cow Pasture", "note", "Let's caw loudly at 7PM", () => console.log("Note delivered.")); لكن يجب تعريف نوع طلب أولًا باسم "note" لتمكين الأعشاش من استقبال ذلك الطلب، كما يجب أن تعمل الشيفرة التي تعالج الطلبات على جميع الأعشاش التي تستطيع استقبال رسالة من هذا النوع وليس على حاسوب العش فقط، حيث سنفترض أنّ الغراب سيطير هنا ليثبِّت شيفرة المعالج تلك على جميع الأعشاش. import {defineRequestType} from "./crow-tech"; defineRequestType("note", (nest, content, source, done) => { console.log(`${nest.name} received note: ${content}`); done(); }); تُعرِّف الدالة defineRequestType نوعًا جديدًا من الطلبات، كما يضيف المثال الدعم لطلبات "note" التي ترسِل تذكرة note إلى العش المعطى، ويَستدعي تطبيقنا console.log كي نستطيع توكيد وصول الطلب، كما تحتوي الأعشاش على الخاصية name التي تحمل أسماءها؛ أما الوسيط الرابع المعطى للمعالج فهو done، وهي دالة رد نداء يجب أن تستدعي عند انتهاء الطلب، فإذا استخدمنا قيمة الإعادة للمعالج على أساس قيمة رد، فهذا يعني عدم استطاعة معالج الطلب تنفيذ إجراءات غير متزامنة بنفسه، فالدالة التي تنفذ مهامًا غير متزامنة تُعيد قبل انتهاء العمل، كما تكون قد جهزت رد نداء عند انتهاءها، لذا فنحن نحتاج إلى آلية غير متزامنة، وهي دالة رد نداء أخرى في حالتنا لترسل إشعارًا حين يتاح ردًا. قد يكون اللاتزامن معديًا contagious نوعًا ما، فأيّ دالة تستدعي دالةً أخرى تعمل بأسلوب غير متزامن يجب أن تكون هي نفسها غير متزامنة، ومستخدمةً لرد نداء أو آليةً مشابهةً لتسليم نتيجتها؛ أما استدعاء رد النداء ففيه تفصيل أكثر من مجرد إعادة قيمة ما، كما يكون عرضةً للأخطاء، لذا فإنّ الحاجة إلى هيكلة أجزاء كبيرة من برنامجنا بهذه الطريقة ليست الأسلوب الأمثل. الوعود يسهل العمل مع المفاهيم المجردة حين يمكن تمثيلها بقيم، وفي حالة الإجراءات غير المتزامنة سنستطيع إعادة كائن يمثل الحدث المستقبلي، بدلًا من إعداد دالة لتُستدعى في نقطة ما في المستقبل، وهذه هي وظيفة الصنف Promise، فالوعد هو إجراء غير متزامن قد يحدث عند نقطة ما ويعطينا قيمة، ويستطيع إبلاغ أيّ أحد يريد إشعاره حين تكون القيمة متاحةً. يُعَدّ استدعاء Promise.resolve أسهل طريقة لإنشاء وعد، وهي دالة تضمن أنّ القيمة التي تعطيها إياها ستغلَّف داخل وعد، فإذا كان هي نفسها وعدًا، فستعاد ببساطة، وإلا فستحصل على وعد ينتهي مباشرةً بقيمتك على أساس نتيجة. let fifteen = Promise.resolve(15); fifteen.then(value => console.log(`Got ${value}`)); // → Got 15 نستطيع استخدام التابع then للحصول على وعد، إذ يسجل دالة رد نداء لتُستدعى حين ينتهي الوعد وينتج قيمةً، كما نستطيع إضافة عدة ردود نداء إلى وعد واحد، إذ ستُستدعى جميعًا حتى لو أضفتها بعد انتهاء الوعد، ومع هذا فلا زال التابع then يحتوي مزيدًا من الخصائص، إذ يُعيد وعدًا آخرًا ينتهي بالقيمة التي تعيدها دالة المعالِج، أو إذا أعاد ذلك وعدًا، فسينتظر ذلك الوعد وينتهي مُخرجًا نتيجته. يُعَدّ النظر للوعود على أنها جهاز لنقل القيم إلى بيئة غير متزامنة مفيدًا، فالقيمة العادية موجودة والقيمة الموعودة promised value قد تكون موجودة، كما قد تظهر في نقطة ما في المستقبل، كما تتصرف الحسابات المعرَّفة بالوعود على مثل هذه القيم المغلَّفة wrapped values وتنفَّذ تنفيذًا غير متزامن عندما تكون القيم متاحة. نستطيع استخدام Promise على أساس باني لكي ننشئ وعدًا، لكن هذا سيكون له واجهة غريبة نوعًا ما، إذ يتوقع الباني دالةً على أساس وسيط ويستدعيها مباشرةً، ثم يمرر إليها دالةً تستطيع استخدامها لحل الوعد وإنهائه، وسبب عملها بهذا الأسلوب بدلًا من استخدام التابع resolve مثلًا، هو اقتصار حل الوعد على الشيفرة التي أنشأته. يوضِّح المثال التالي كيفية إنشاء واجهة مبنية على وعد لدالة readStorage: function storage(nest, name) { return new Promise(resolve => { nest.readStorage(name, result => resolve(result)); }); } storage(bigOak, "enemies") .then(value => console.log("Got", value)); تُعيد هذه الدالة غير المتزامنة قيمةً ذات معنى، وهذه هي الميزة الأساسية للوعود، إذ تبسِّط استخدام الدوال غير المتزامنة، فبدلًا من الحاجة إلى تمرير ردود النداء كما تبدو الدوال المبنية على الوعود أشبه بالدوال العادية؛ فهي تأخذ الدخل على أساس وسائط وتُعيد مخرجاتها، ولا تختلف إلا أنّ الخرج قد يكون غير متاح بعد. الفشل Failure يمكن أن تفشل حسابات جافاسكربت العادية برفع استثناء exception، إذ تحتاج الحوسبة غير المتزامنة إلى ذلك عادةً، فقد يفشل طلب الشبكة أو شيفرة ترفع استثناءً وتكون جزءًا من الحوسبة غير المتزامنة. إحدى المشاكل المؤرقة في نمط رد النداء للبرمجة غير المتزامنة أنها تصعِّب ضمان الإبلاغ الصحيح لردود النداء بشأن حالات الفشل، وأحد الحلول المستخدَمة على نطاق واسع هو استخدام وسيط رد النداء الأول لتوضيح فشل الإجراء، بعدها سيحتوي الوسيط الثاني على القيمة التي أنتجها الإجراء حين كان ناجحًا. يجب أن تتحقق دوال ردود النداء هذه إذا كانت قد استقبلت استثناءً أم لا، كما عليها التأكد من أنّ أيّ مشكلة تسببها بما فيها الاستثناءات المرفوعة من قِبَل الدوال التي استدعتها، قد عُلم بها وسُلِّمت إلى الدالة المناسبة؛ أما الوعود فتيسِّر من هذه العملية كثيرًا، فهي إما محلولة -أي انتهى الإجراء بنجاح-، أو مرفوضة -أي فشلت-، إذ لا تُستدعى معالِجات الإنهاء resolve handlers التي سُجلت في then إلا عند نجاح الإجراء؛ أما عمليات الرفض فتُنقَل إلى الوعد الجديد الذي تُعيده then. وحين يرفع معالج استثناءً، فسيجعل هذا الوعد الذي أنتجه الاستدعاء إلى then مرفوضًا. لهذا إذا فشل أيّ عنصر في سلسلة إجراءات غير متزامنة، فسيحدَّد الناتج الكلي للسلسلة على أنه مرفوض، ولا تُستدعى معالِجات نجاح أبعد من النقطة التي فشلت فيها. كذلك فإنّ رَفْض وعد ما ينتج قيمةً تمامًا مثل التي ينتجها حل الوعد، كما يُطلق على تلك القيمة سبب الرفض، فإذا تسبب استثناء في دالة معالِجة بالرفض، فستُستخدِم قيمة الاستثناء على أساس سبب، وبالمثل، إذا أعاد معالج وعدًا مرفوضًا، فسينتقل ذلك الرفض إلى الوعد التالي، حيث لدينا دالة Promise.reject التي تنشئ وعدًا جديدًا مرفوضًا فورًا. تحتوي الوعود على التابع catch لمعالجة عمليات الرفض تلك صراحةً، إذ يسجِّل معالجًا ليُستدعى حين يُرفض الوعد مثلما تعالج معالجات then الحل العادي للوعود، كما تشبه then في أنها تُعيد وعدًا جديدًا، إذ يحل إلى قيمة الوعد الأصلية إذا حل حلًا عاديًا، وإلى نتيجة معالج catch في غير ذلك. يُرفَض الوعد الجديد كذلك إذا رفع معالج catch خطأً ما، وللاختصار، تقبل then معالج الرفض على أساس وسيط ثاني، لذا نستطيع تثبيت كلا النوعين من المعالجات في استدعاء تابع وحيد، كما تستقبل الدالة الممرَّرة إلى الباني Promise وسيطًا ثانيًا مع دالة حل resolve function لتستخدمها في رفض الوعد الجديد. يمكن النظر إلى سلاسل قيم الوعود المنشأة باستدعاءات إلى then وcatch على أساس خط أنابيب تتحرك فيه عمليات الفشل أو القيم غير المتزامنة، وبما أنّ تلك السلاسل قد أُنشئت بتسجيل معالجات، فسيكون لكل رابط معالج نجاح أو معالج فشل أو كليهما؛ حيث تُهمَل المعالِجات التي لا تطابق نوع الخرج -بنجاح أو فشل-؛ أما المعالجات التي تطابق فستُستدعى ويحدِّد خرجها نوع القيمة التي ستأتي تاليًا، بحيث تكوّن نجاحًا حين تُعيد قيمةً ليست وعدًا، وفشلًا حين ترفع استثناءً، وخرْج الوعد حين تُعيد أحد هذين. new Promise((_, reject) => reject(new Error("Fail"))) .then(value => console.log("Handler 1")) .catch(reason => { console.log("Caught failure " + reason); return "nothing"; }) .then(value => console.log("Handler 2", value)); // → Caught failure Error: Fail // → Handler 2 nothing تُعالَج الاستثناءات غير الملتقَطة بواسطة البيئة، إذ تستطيع بيئات جافاسكربت استشعار إذا لم يعالَج رفض الوعد، وستُبلِّغ هذا في صورة خطأ. صعوبة الشبكات قد تأتي أيام لا يكون فيها ضوء كافي لنظام المرايا الذي يستخدمه الغربان من أجل نقل إشارة، أو قد يحجب شيء ما مسار الإشارة، كذلك قد تُرسَل إشارة ولا تُستقبَل من الطرف الآخر، حيث سيتسبب ذلك في عدم استدعاء رد النداء المعطى إلى send، وهو الأمر الذي سيجعل البرنامج يتوقف دون أي ملاحظة حتى لوجود مشكلة، فلو بلَّغ الطلب بالفشل إذا مر زمن محدد دون رد، لكان أفضل. تكون حالات فشل الإرسال في العادة حوادث عرضية مثل تداخل الأضواء القادمة من مصابيح سيارة مع الإشارات الضوئية، وهنا سينجح الطلب بمجرد إعادة المحاولة، لذا سنجعل دالة الطلب تحاول تلقائيًا إعادة إرسال الطلب عدة مرات قبل أن تستسلم. وبما أننا عرفنا الآن أن الوعود أمر جيد، فسنجعل دالة الطلب تُعيد وعدًا، إذ يتشابه رد النداء والوعد من حيث ما يستطيعان التعبير عنه، كما يمكن تغليف الدوال المبنية على ردود النداء لكشف واجهة مبنية على وعد، والعكس صحيح. قد يشير الرد إلى فشل عند تسليم الطلب ورده بنجاح إذا حاول الطلب استخدام نوع طلب غير معرَّف أو رفع المعالج خطأ مثلًا. لدعم هذا يتَّبع كل من send وdefineRequestType الأسلوب المذكور أعلاه، حيث يكون الوسيط الأول الممرَّر إلى ردود النداء هو سبب الفشل إذا وُجد، في حين يكون الثاني هو النتيجة الحقيقية، ويمكن ترجمة هذا إلى حل الوعد ورفضه باستخدام المغلِّف الخاص بنا. class Timeout extends Error {} function request(nest, target, type, content) { return new Promise((resolve, reject) => { let done = false; function attempt(n) { nest.send(target, type, content, (failed, value) => { done = true; if (failed) reject(failed); else resolve(value); }); setTimeout(() => { if (done) return; else if (n < 3) attempt(n + 1); else reject(new Timeout("Timed out")); }, 250); } attempt(1); }); } سينجح ما فعلناه أعلاه لأنّ الوعود لا يمكن حلها أو رفضها إلا مرةً واحدة، إذ تُحدِّد المرة الأولى التي يُستدعى فيها resolve أو reject خرج الوعد، كما تُتجاهل الاستدعاءات التالية التي سببها طلب عائد بعد انتهاء طلب آخر. سنحتاج إلى استخدام دالة تعاودية لبناء حلقة غير متزامنة من أجل إعادة المحاولة التي ذكرناها، حيث لا تسمح الحلقة العادية بالتوقف وانتظار الإجراء غير المتزامن، وتنفِّذ الدالة attempt محاولةً واحدةً لإرسال الطلب وتضبط زمن مهلة محدد، فإذا لم تحصل على استجابة أو رد بعد ربع ثانية، فستبدأ المحاولة الثانية أو ترفض الوعد إذا كانت هذه هي المحاولة الثالثة، ويكون السبب هنا هو نسخة من Timeout. تُعَدّ المحاولة كل ربع ثانية وتركها إذا لم نتلق ردًا بعد ثلاثة أرباع ثانية نظامًا عشوائيًا نوعًا ما، فمن المحتمل إذا جاء الطلب لكن المعالج استغرق وقتًا أطول قليلًا أن تُسلَّم الطلبات أكثر من مرة، وسنكتب معالجاتنا واضعين تلك المشكلة في حساباتنا، فلا يجب أن تمثل الرسائل المكررة مشكلة. لن نبني شبكة عالية المستوى هنا، فسقف توقعات الغربان ليس عاليًا على أي حال حين يتعلق الأمر بالحوسبة، ولكي نعزل أنفسنا من ردود النداء كلها، فسنعرِّف مغلِّفًا لـ defineRequestType يسمح لدالة المعالِج أن تُعيد وعدًا أو قيمةً مجردةً plain value ويوصل ذلك لرد النداء من أجلنا. function requestType(name, handler) { defineRequestType(name, (nest, content, source, callback) => { try { Promise.resolve(handler(nest, content, source)) .then(response => callback(null, response), failure => callback(failure)); } catch (exception) { callback(exception); } }); } نستخدِم Promise.resolve لتحويل القيمة التي يُعيدها handler إلى وعد إذا لم تُحوَّل فعليًا. لاحظ أنه يجب تغليف الاستدعاء إلى handler في كتلة try لضمان توصيل أي استثناءات يرفعها مباشرةً إلى رد النداء، حيث يوضح هذا صعوبة معالجة الأخطاء التي فيها ردود نداء معالجة صحيحة، ومن السهل نسيان كيفية توجيه استثناءات مثل هذه، وإذا لم نفعل ذلك فلن يُبلّغ بحالات الفشل إلى رد نداء مناسب؛ أما الوعود فستجعل هذا آليًا تقريبًا ومن ثم يكون أقل عرضةً للأخطاء. تجميعات الوعود يحتفظ حاسوب العش بمصفوفة من الأعشاش الأخرى التي في نطاق الاتصال في الخاصية neighbors. سنكتب دالةً تحاول إرسال طلب "ping" إلى كل حاسوب لنرى أيها ترد علينا، وهو طلب يسأل ردًا ببساطة، وذلك لنرى إن كان مكن الوصول إلى أيّ من تلك الأعشاش. تُعَدّ الدالة Promise.all مفيدةً عند العمل مع تجميعات من الوعود التي تعمل في نفس الوقت، إذ تُعيد وعدًا ينتظر حل جميع الوعود التي في المصفوفة، ثم يُحل هو إلى مصفوفة من القيم التي أنتجتها تلك الوعود بترتيب المصفوفة الأصلية نفسها، وإذا رُفض أي وعد فستُرفَض نتيجة Promise.all كذلك. requestType("ping", () => "pong"); function availableNeighbors(nest) { let requests = nest.neighbors.map(neighbor => { return request(nest, neighbor, "ping") .then(() => true, () => false); }); return Promise.all(requests).then(result => { return nest.neighbors.filter((_, i) => result[i]); }); } لا نريد فشل الوعد المجمَّع إذا كان أحد الجيران غير متاح ونحن ما زلنا لا نعرف باقي البيانات، لذا تُلحِق الدالة التي رُبطت بمجموعة من الجيران لتحولهم إلى وعود طلبات، حيث أن المعالجات تصنع طلبات ناجحة تنتج true وطلبات فاشلة تنتج false. يُستخدَم filter في معالج الوعد المجمَّع لحذف العناصر من مصفوفة neighbors التي تكون قيمتها الموافقة هي false، إذ يستفيد هذا من كون filter يمرر فهرس مصفوفة العنصر الحالي على أساس وسيط ثاني إلى دالة الترشيح، كما يفعل map وsome وتوابع المصفوفات العليا المشابهة. إغراق الشبكة Network flooding يحد اقتصار تكلم الأعشاش لجيرانها كثيرًا من فائدة هذه الشبكة، وعلى ذلك فربما نحاول إعداد نوع من الطلبات يُعيد توجيه الطلبات تلقائيًا إلى الجيران، وذلك على أساس حل لنشر المعلومات إلى الشبكة كلها، ثم يُعيد هؤلاء الجيران توجيهها إلى جيرانهم، وهكذا حتى تستقبل الشبكة كلها تلك الرسالة. import {everywhere} from "./crow-tech"; everywhere(nest => { nest.state.gossip = []; }); function sendGossip(nest, message, exceptFor = null) { nest.state.gossip.push(message); for (let neighbor of nest.neighbors) { if (neighbor == exceptFor) continue; request(nest, neighbor, "gossip", message); } } requestType("gossip", (nest, message, source) => { if (nest.state.gossip.includes(message)) return; console.log(`${nest.name} received gossip '${ message}' from ${source}`); sendGossip(nest, message, source); }); يحتفظ كل عش بمصفوفة من سلاسل gossip النصية التي رآها فعليُا، وذلك لتجنب تكرار إرسال الرسالة عبر الشبكة للأبد، كما نستخدِم دالة everywhere لتعريف تلك المصفوفة، إذ تُشغِّل هذه الدالة الشيفرة على كل عش من أجل إضافة خاصية إلى كائن state الخاص بالعش، وهو المكان الذي سنحفظ فيه حالة العش المحلية. يتجاهل العش رسالة gossip مكرَّرة مرسَلة إليه، الأمر الذي يحدث عادةً حين يعيد الناس إرسالها دون وعي، لكن حين يستلم رسالةً جديدةً، فسيخبر جميع جيرانه عدا ذلك الذي أرسلها. سينشر هذا جزءًا جديدًا من gossip في الشبكة كما تنتشر بقعة الحبر في الماء، فحتى إذا لم تكن بعض الاتصالات عاملةً وقتها، فستصل gossip العش المعطى إذا كان ثمة طريق بديلة إليه. يسمى ذلك الأسلوب في التواصل الشبكي باسم الإغراق أو الغمر flooding، فهو يغرق الشبكة بجزء من المعلومات إلى أن تكون لدى جميع العقد. نستطيع استدعاء sendGossip لنرى تدفق الرسائل خلال القرية. sendGossip(bigOak, "Kids with airgun in the park"); توجيه الرسائل إذا أرادت عقدة ما التواصل مع عقدة أخرى فلن يكون أسلوب الإغراق هنا هو الأمثل، خاصةً حين تكون الشبكة كبيرة، فقد يؤدي هذا إلى كثير من نقل البيانات غير الضروري، والحل هنا هو إعداد طريقة تنتقل بها الرسائل من عقدة إلى عقدة حتى تصل إلى وجهتها. تكون الصعوبة في ذلك أنها ستحتاج إلى معرفة بتخطيط الشبكة، حيث من الضروري لإرسال طلب في اتجاه عش بعيد، معرفة أيّ الأعشاش الجارة قادرة على إيصاله بصورة أسرع إلى وجهته، حيث سنهدر كثيرًا من الموارد دون جدوى إذا أرسلناه في الاتجاه الخاطئ. لا يملك العش المعلومات التي يحتاجها لحساب الطريق لأنه لا يعرف إلا جيرانه المباشَرِين، وعليه يجب نشر معلومات الاتصال لكل الأعشاش بطريقة تسمح لها بالتغير مع مرور الوقت كلما هُجِر عش أو بُني آخر. نستطيع استخدام الإغراق هنا مرةً أخرى، لكننا سننظر هل تطابق مجموعة الجيران الجديدة لعش ما المجموعة الحالية التي لدينا، أم لا بدلًا من التحقق هل تم استلام الرسالة أم لا. requestType("connections", (nest, {name, neighbors}, source) => { let connections = nest.state.connections; if (JSON.stringify(connections.get(name)) == JSON.stringify(neighbors)) return; connections.set(name, neighbors); broadcastConnections(nest, name, source); }); function broadcastConnections(nest, name, exceptFor = null) { for (let neighbor of nest.neighbors) { if (neighbor == exceptFor) continue; request(nest, neighbor, "connections", { name, neighbors: nest.state.connections.get(name) }); } } everywhere(nest => { nest.state.connections = new Map(); nest.state.connections.set(nest.name, nest.neighbors); broadcastConnections(nest, nest.name); }); تستخدِم الموازنة JSON.stringify لأنّ == لن يُعيد القيمة true إلا إذا كان الاثنان نفس القيمة بالضبط سواءً كان على كائنات أو مصفوفات، وليس هذا ما نريده هنا، وبالتالي فقد تكون موازنة سلاسل JSON النصية بدائيةً قليلًا لكنها فعالة لموازنة محتواها. تبدأ العقد بنشر اتصالاتها مباشرةً، مما يعطي كل عش خريطةً بمخطط الشبكة الحالي، ما لم يوجد عش منها يستحيل الوصول إليه، كما نستطيع باستخدام ذلك المخطط إيجاد الطرق فيه كما رأينا في مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت، فإذا كان لدينا طريق نحو وجهة رسالة ما، فسنستطيع معرفة أي اتجاه نرسلها فيه. تبحث الدالة findRoute -التي تشابه findRoute من مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت- عن طريقة للوصول إلى عقدة معطاة في الشبكة، لكن بدلًا من إعادة الطريق كله، فهي تعيد الخطوة التالية فقط، وسيقرِّر العش التالي مستخدِمًا المعلومات المتوفرة لديه عن الشبكة أين سيرسل الرسالة تاليًا. function findRoute(from, to, connections) { let work = [{at: from, via: null}]; for (let i = 0; i < work.length; i++) { let {at, via} = work[i]; for (let next of connections.get(at) || []) { if (next == to) return via; if (!work.some(w => w.at == next)) { work.push({at: next, via: via || next}); } } } return null; } نستطيع الآن بناء دالة يمكنها إرسال رسائل بعيدة المدى، فإذا كانت مرسَلةً إلى جار مباشر، فستسلَّم مثل العادة؛ أما إذا كان إلى جار بعيد، فستُحزَّم في كائن وترسَل إلى جار قريب من الهدف باستخدام نوع الطلب "route" الذي سيجعل ذلك الجار يكرِّر السلوك نفسه. function routeRequest(nest, target, type, content) { if (nest.neighbors.includes(target)) { return request(nest, target, type, content); } else { let via = findRoute(nest.name, target, nest.state.connections); if (!via) throw new Error(`No route to ${target}`); return request(nest, via, "route", {target, type, content}); } } requestType("route", (nest, {target, type, content}) => { return routeRequest(nest, target, type, content); }); نستطيع الآن إرسال الرسالة إلى العش الذي في برج الكنيسة، حيث سنكون قد وفرنا أربع محطات من الطريق في الشبكة. routeRequest(bigOak, "Church Tower", "note", "Incoming jackdaws!"); لهذا نكون قد بنينا عدة طبقات من الوظائف فوق نظام اتصالات بدائي من أجل تسهيل استخدامه، وهذا نموذج جميل لكيفية عمل شبكات الحواسيب الحقيقية رغم بساطته أو سطحيته، لكن الخاصية التي تميز شبكات الحواسيب أنها لا يُعتمد عليها، فرغم استطاعتنا على بناء تجريدات abstractions فوقها لتساعدنا، إلا أنه لا يمكننا منع فشل الشبكة، لذا تدور برمجة الشبكات حول توقُّع حالات الفشل والتعامل معها. دوال async تنسخ الغربان المعلومات المهمة في الأعشاش من أجل الحفاظ عليها إذا هجم صقر ما على أحدها ودمره، فإذا أرادت استرجاع معلومات ليست موجودة في حوصلة التخزين الخاصة بها، فسيسأل حاسوب العش بقية الأعشاش في الشبكة حتى يجد واحدًا منها لديه تلك المعلومات. requestType("storage", (nest, name) => storage(nest, name)); function findInStorage(nest, name) { return storage(nest, name).then(found => { if (found != null) return found; else return findInRemoteStorage(nest, name); }); } function network(nest) { return Array.from(nest.state.connections.keys()); } function findInRemoteStorage(nest, name) { let sources = network(nest).filter(n => n != nest.name); function next() { if (sources.length == 0) { return Promise.reject(new Error("Not found")); } else { let source = sources[Math.floor(Math.random() * sources.length)]; sources = sources.filter(n => n != source); return routeRequest(nest, source, "storage", name) .then(value => value != null ? value : next(), next); } } return next(); } ولأن connections هي خارطة Map، فلن تعمل Object.keys عليها، لكن لديها التابع keys الذي يعيد مكرِّرًا iterator وليس مصفوفةً، كما يمكن تحويله إلى مصفوفة باستخدام الدالة Array.from، وهي شيفرة غير مألوفة حتى مع الوعود، إذ تُسلسَل عدة إجراءات غير متزامنة معًا بطرق غير واضحة، وسنحتاج إلى الدالة التعاودية next لوضع نموذج المرور الحلَقي على الأعشاش؛ أما ما تفعله الشيفرة على الحقيقة فهو سلوك خطي، إذ تنتظر تمام الإجراء السابق قبل بدء الذي يليه، وبالتالي ستكون أسهل في التعبير عنها في نموذج برمجة متزامنة. الجميل في الأمر هو سماح جافاسكربت بكتابة شيفرة وهمية متزامنة pseudo-synchronous لوصف الحوسبة غير المتزامنة، إذ تُعيد الدالة async -ضمنيًا- وعدًا وتنتظر await في متنها وعودًا أخرى بطريقة تبدو تزامنية. نُعيد كتابة findInStorage كما يلي: async function findInStorage(nest, name) { let local = await storage(nest, name); if (local != null) return local; let sources = network(nest).filter(n => n != nest.name); while (sources.length > 0) { let source = sources[Math.floor(Math.random() * sources.length)]; sources = sources.filter(n => n != source); try { let found = await routeRequest(nest, source, "storage", name); if (found != null) return found; } catch (_) {} } throw new Error("Not found"); } تُحدَّد الدالة async بكلمة async قبل كلمة function المفتاحية، كما يمكن جعل التوابع غير تزامنية بكتابة async قبل أسمائها. وإذا استدعيت دالةً مثل تلك؛ فستُعيد وعدًا، وكذلك الحال بالنسبة للتوابع، ويُحل الوعد عندما تُعيد الدالة شيئًا؛ أما إذا رفعت الدالة استثناءً فسيُرفض الوعد. findInStorage(bigOak, "events on 2017-12-21") .then(console.log); يمكن وضع كلمة await المفتاحية أمام تعبير ما داخل دالة async لجعله ينتظر حل وعد ما، ولا يتابع تنفيذ الدالة إلا بعد حل ذلك الوعد. لم تَعُد مثل تلك الدوال تعمل من البداية للنهاية على مرة واحدة، شأنها شأن أيّ دالة جافاسكربت عادية، إذ يمكن تجميدها frozen في أي نقطة توجد فيها await، وبعدها ستستعيد العمل في وقت لاحق؛ أما بالنسبة للشيفرات غير المتزامنة المهمة، فتلك الصيغة أسهل من استخدام الوعود مباشرةً، حتى لو احتجنا إلى فعل شيء لا يناسب النموذج المتزامن مثل تنفيذ عدة إجراءات في الوقت نفسه، إذ من السهل دمج await مع الاستخدام المباشر للوعود. المولدات Generators إن قدرة الدالة على التوقف ثم المتابعة مرةً أخرى ليست حكرًا على دوال async وحدها، حيث تحتوي جافاسكربت على خاصية اسمها دوال المولِّد generator functions التي تشبهها لكن مع استثناء الوعود، فحين نعرِّف دالةً باستخدام ‎function*‎ -أي أننا سنضع محرف النجمة بعد الكلمة function لستصير مولِّدًا، ويُعيد مكررًا عند استدعائه كما رأينا في مقال الحياة السرية للكائنات في جافاسكريبت. function* powers(n) { for (let current = n;; current *= n) { yield current; } } for (let power of powers(3)) { if (power > 50) break; console.log(power); } // → 3 // → 9 // → 27 تُجمَّد الدالة في بدايتها حين نستدعي powers، وتعمل في كل مرة تستدعي next على المكرِّر حتى تقابل تعبير yield الذي يوقفها ويجعل القيمة المحصَّلة هي القيمة التي ينتجها المكرِّر تاليًا، وحين تُعيد الدالة -علمًا أنّ دالة مثالنا لا تعيد- فسيكون المكرِّر قد انتهى. تُعَدّ كتابة المكرِّرات أسهل كثيرًا من استخدام الدوال المولِّدة، حيث يمكن كتابة مكرِّر الصنف Group من التدريب في مقال الحياة السرية للكائنات في جافاسكريبت بهذا المولِّد: Group.prototype[Symbol.iterator] = function*() { for (let i = 0; i < this.members.length; i++) { yield this.members[i]; } }; لم نعد في حاجة إلى إنشاء كائن ليحفظ حالة المكرِّر، إذ تحفظ المولِّدات حالتها المحلية تلقائيًا في كل مرة تُحصِّل فيها، وقد لا تقع تعبيرات مثل yield تلك إلا مباشرةً في دالة المولِّد نفسها وليس في دالة داخلية معرَّفة داخلها، والحالة التي يحفظها المولِّد عند التحصيل هي بيئته المحلية فقط والموضع الذي حَصَّل فيه؛ أما دالة async فهي نوع خاص من المولِّدات، فهي تُنتِج وعدًا عند استدعائها، كما يُحَل عند إعادتها -أي انتهائها-، في حين يُرفَض إذا رفعت استثناءً، ونحصل على ناتج الوعد سواء كان قيمة أو استثناءً مرفوعًا باستعمال التعبير awaits مع الوعد الذي تعيده الدالة. حلقة الحدث التكرارية تُنفَّذ البرامج غير المتزامنة جزءًا جزءًا، وقد يبدأ كل جزء ببعض الإجراءات ويجدول شيفرات لتنفيذها عند انتهاء الإجراء أو فشله؛ أما بين تلك الأجزاء فسيكون البرنامج ساكنًا منتظِرًا الإجراء التالي، وبالتالي لا تُستدعى ردود النداء من قِبَل الشيفرة التي جدولَتها، فإذا استدعينا setTimeout من داخل دالة، فستكون تلك الدالة قد أعادت بالوقت الذي تُستدعى فيه دالة رد النداء، وحين يُعيد رد النداء، فلن يعود التحكم إلى الدالة التي جدولته. يحدث السلوك غير المتزامن في مكدس استدعاء الدوال الفارغ الخاص به، وهذا أحد الأسباب التي تصعب معها إدارة الاستثناءات في الشيفرات غير المتزامنة في غياب الوعود، إذ لن تكون معالجات catch الخاصة بنا في المكدس حين ترفع استثناءً بما أنّ كل رد نداء يبدأ بمكدس شبه فارغ. try { setTimeout(() => { throw new Error("Woosh"); }, 20); } catch (_) { // لن يعمل هذا console.log("Caught!"); } مهما كانت أوقات وقوع الأحداث -مثل طلبات timeouts أو incoming- متقاربة، فلن تشغّل بيئة جافاسكربت إلا برنامجًا واحدًا في كل مرة. يمكن النظر إلى هذا السلوك على أنه يشغِّل حلقةً تكراريةً كبيرةً على برنامجنا يُطلق عليها حلقة الحدث event loop، فإذا لم يكن ثمة شيء لفعله فستتوقف الحلقة؛ أما إذا جاءت الأحداث، فستضاف إلى طابور queue وتنفَّذ شيفراتها واحدةً تلو الأخرى، ولأنه لا يمكن تنفيذ شيئين في الوقت نفسه، فستؤخر الشيفرة البطيئة التنفيذ معالجة الأحداث الأخرى. يضبط المثال أدناه زمن إنهاء timeout، لكن يتلكأ بعد ذلك إلى ما بعد النقطة الزمنية المحدَّدة له مسببًا تأخر زمن الإنهاء. let start = Date.now(); setTimeout(() => { console.log("Timeout ran at", Date.now() - start); }, 20); while (Date.now() < start + 50) {} console.log("Wasted time until", Date.now() - start); // → Wasted time until 50 // → Timeout ran at 55 تُحل الوعود دائمًا أو تُرفض على أساس حدث جديد، وحتى لو حُل الوعد، فسسيتسبب انتظاره في تشغيل رد النداء الخاص بك بعد انتهاء السكربت الحالية بدلًا من تشغيله مباشرةً. Promise.resolve("Done").then(console.log); console.log("Me first!"); // → Me first! // → Done سنرى في الفصول اللاحقة عدة أنواع من الأحداث التي تعمل على حلقة الحدث. زلات البرامج غير المتزامنة لا توجد تغيرات حادثة في الحالة إذا كان البرنامج يعمل بتزامن على مرة واحدة عدا تلك التي يصنعها البرنامج نفسه؛ أما بالنسبة للبرامج غير المتزامنة فالأمر مختلف، إذ قد تحتوي على فجوات gaps في تنفيذها يمكن لشيفرات أخرى العمل خلالها. لننظر إلى المثال التالي، إذ يهوى أحد الغربان عدّ الفراخ التي تفقس في القرية كل عام، حيث تخزِّن الأعشاش هذا العدد في حواصل التخزين الخاصة بها، وتحاول الشيفرة التالية حساب العدد من جميع الأعشاش لأحد الأعوام. function anyStorage(nest, source, name) { if (source == nest.name) return storage(nest, name); else return routeRequest(nest, source, "storage", name); } async function chicks(nest, year) { let list = ""; await Promise.all(network(nest).map(async name => { list += `${name}: ${ await anyStorage(nest, name, `chicks in ${year}`) }\n`; })); return list; } يُظهر الجزء async name‎ =>‎ أنّ الدوال السهمية يمكن أن تكون غير متزامنة إذا وضعنا كلمة async أمامها. لن يبدو أيّ شيء غريبًا على الشيفرة لأول وهلة، فهي توجه الدوال السهمية غير المتزامنة async خلال مجموعة من الأعشاش لتنشئ مصفوفةً من الوعود، ثم ستستخدِم Promise.all لتنتظر حل كل تلك الوعود قبل إعادتها القائمة التي جهزتها، غير أنّ هذا السلوك معيب، إذ يُعيد سطرًا واحدًا يحتوي دائمًا على العش الأبطأ -الذي يصل رده متأخرًا- في الإجابة. chicks(bigOak, 2017).then(console.log); تكمن المشكلة في العامِل ‎+=‎ الذي يأخذ قيمة list الحالية وقت بدء تنفيذ التعليمة، ثم يضبط رابطة list بعد انتهاء await لتكون تلك القيمة إضافةً إلى السلسلة النصية المضافة؛ أما في الوقت الذي يكون بين بدء تنفيذ التعليمة والوقت الذي تنتهي فيه، سيكون لدينا فجوة غير متزامنة. يعمل التعبير map قبل أي شيء أضيف إلى القائمة، لذا يبدأ كل عامل من العوامل ‎+=‎ بسلسلة فارغة، وينتهي حين تنتهي عملية استعادة التخزين بضبط قائمة list من سطر واحد، حيث تكون نتيجة إضافة السطر إلى السلسلة الفارغة. كان يمكن تجنب ذلك بسهولة بإعادة الأسطر من الوعود الموجَّهة واستدعاء join على نتيجة Promise.all بدلًا من بناء القائمة بتغيير رابطة ما، كما سيكون حساب قيمة جديدة أقل عرضةً للخطأ من تغيير القيم الموجودة سلفًا. async function chicks(nest, year) { let lines = network(nest).map(async name => { return name + ": " + await anyStorage(nest, name, `chicks in ${year}`); }); return (await Promise.all(lines)).join("\n"); } تحدث أخطاء مثل هذه بسهولة خاصةً عند استخدام await، كما يجب أن نكون على علم بأماكن حدوث الفجوات في شيفراتنا، وإحدى مزايا اللاتزامن الصريح في جافاسكربت، هي أنّ العثور على تلك الفجوات سهل نسبيًا، إذ سيكون إما باستخدام ردود النداء أو الوعود أو await. خاتمة تمكننا البرمجة غير التزامنية من التعبير عن الانتظار للإجراءات actions التي تعمل لوقت طويل دون تجميد البرنامج أثناء تلك الإجراءات، وتنفّذ بيئات جافاسكربت ذلك النمط من البرمجة باستخدام ردود النداء، وهي الدوال التي تُستدعى عند تمام الإجراءات. تُجدوِل حلقة الحدث ردود النداء لتُستدعى في الوقت المناسب واحدًا تلو الآخر كي لا يتداخل تنفيذها، كما صارت البرمجة غير التزامنية سهلةً بفضل الوعود التي هي كائنات تمثّل إجراءات قد تكتمل في المستقبل، ودوال async التي تسمح لنا بكتابة برنامج غير متزامن كما لو كان تزامنيًا. تدريبات تتبع المشرط تملك غربان القرية مشرطًا قديمًا تستخدِمه أحيانًا في المهام الخاصة مثل التحزيم أو قطع الأسلاك، كما تريد الحفاظ عليه لئلا يضيع، لذلك، فكلما نُقل المشرط إلى عش جديد أُضيف مُدخل جديد في ذاكرة العش القديم والجديد معًا تحت اسم مشرط "scalpel"، حيث تكون قيمته هي موقعه الجديد، ويعني هذا أنّ إيجاد المشرط مسألة اتباع أثر مدخلات الذاكرة حتى تجد عشًا يشير إلى نفسه. اكتب دالة async اسمها locateScalpel تفعل ذلك، بحيث تبدأ من العش الذي تشغّله، كما يمكنك استخدام الدالة anyStorage التي عرَّفناها من قبل للوصول إلى الذاكرة في أعشاش عشوائية، وافترض أن المشرط قد دار لمدة تكفي لكي يحتوي كل عش على مُدخل "scalpel" في ذاكرة بياناته، ثم اكتب الدالة نفسها مرةً أخرى دون استخدام async وawait. هل تظهر طلبات الفشل مثل رفض للوعود المعادة في كلا النسختين من الدالة؟ وكيف؟ تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. async function locateScalpel(nest) { // شيفرتك هنا } function locateScalpel2(nest) { // شيفرتك هنا } locateScalpel(bigOak).then(console.log); // → Butcher Shop إرشادات للحل يمكن تنفيذ هذا التدريب بحلقة واحدة تبحث في الأعشاش وتنتقل من العش لغيره حين تجد قيمةً لا تطابق اسم العش الحالي، وتعيد اسم العش حين تجد قيمة مطابِقة، كما يمكن استخدام حلقة while أو for عادية في دالة async. عليك بناء حلقتك باستخدام دالة تعاودية لتنفيذ الشيء نفسه في الدالة المجردة plain function، وأسهل طريقة لذلك هي جعل تلك الدالة تعيد وعدًا باستدعاء then على الوعد الذي يجلب قيمة الذاكرة. سيُعيد المعالج تلك القيمة أو وعدًا مستقبليًا يُنشأ باستدعاء الدالة الحلقية مرةً أخرى بناءً على مطابقة القيمة لاسم العش الحالي. لا تنسى بدء الحلقة باستدعاء الدالة التعاودية مرة واحدة من الدالة الأساسية. تحوِّل await الوعود المرفوضة في دالة async إلى استثناءات، وحين ترفع دالة async استثناءً سيُرفض وعدها، وعليه تكون هذه الطريقة ناجحةً. إذا استخدمت دالة ليست async كما وضحنا سابقًا، فستتسبب الطريقة التي تعمل بها then تلقائيًا في وجود الفشل في الوعد المعاد، وإذا فشل الطلب فلن يُستدعى المعالِج الممرَّر إلى then، ويُرفض الوعد الذي يعيده للسبب نفسه. بناء Promise.all إذا كانت لدينا مصفوفة من الوعود، فإن Promise.all تعيد وعدًا ينتظر جميع الوعود الأخرى في المصفوفة حتى انتهائها، حيث سينجح ذلك الوعد ويُحصِّل مصفوفةً من القيم الناتجة، فإذا فشل وعد في المصفوفة، فسيفشل الوعد المعاد من all أيضًا، ويكون سبب الفشل هو الوعد الفاشل. استخدِم شيئًا مثل ذلك بنفسك في صورة دالة عادية اسمها Promise_all، وتذكَّر أنّ الوعد لا يمكن أن ينجح أو يفشل بعد نجاحه أو فشله أول مرة، وأنّ الاستدعاءات إلى الدوال التي تحله تُتَجاهل، إذ يُبسِّط ذلك الطريقة التي تعالج بها فشل وعدك. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. function Promise_all(promises) { return new Promise((resolve, reject) => { // شيفرتك هنا. }); } // شيفرة الاختبار. Promise_all([]).then(array => { console.log("This should be []:", array); }); function soon(val) { return new Promise(resolve => { setTimeout(() => resolve(val), Math.random() * 500); }); } Promise_all([soon(1), soon(2), soon(3)]).then(array => { console.log("This should be [1, 2, 3]:", array); }); Promise_all([soon(1), Promise.reject("X"), soon(3)]) .then(array => { console.log("We should not get here"); }) .catch(error => { if (error != "X") { console.log("Unexpected failure:", error); } }); إرشادات للحل ستُستدعى then من قِبَل الدالة التي تمرَّر إلى باني Promise وذلك على كل وعد في المصفوفة المعطاة، وعند نجاح أحد تلك الوعود، فيجب تخزين القيمة الناتجة في الموضع الصحيح في المصفوفة الناتجة، كما يجب التحقق هل كان ذلك هو الوعد الأخير المنتظِر أم لا، وننهي وعدنا الخاص إن كان. ويمكن تنفيذ ذلك التحقق الأخير بعدَّاد يبدأ بطول مصفوفة الدخل، ونطرح منه 1 في كل مرة ينجح فيها أحد الوعود، ونكون قد انتهينا عند وصوله إلى الصفر. تأكد أن تضع في حسابك موقفًا تكون فيه مصفوفة الدخل فارغة من الوعود، فكيف تُحل الوعود وهي غير موجودة؟ تتطلب معالجة حالات الفشل بعض التأني، لكنها بسيطة في الوقت نفسه، إذ ما عليك سوى تمرير دالة reject للوعد المغلِّف إلى كل الوعود الموجودة في المصفوفة على أساس معالِج catch، أو على أساس وسيط ثاني إلى then، فإذا حدث فشل في أحدها، فسيطلق رفضًا للوعد المغلِّف كله. ترجمة -بتصرف- للفصل الحادي عشر من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا المقال السابق: الوحدات Modules في جافاسكريبت المكررات iterators والمولدات generators غير المتزامنة في جافاسكربت
    1 نقطة
×
×
  • أضف...