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

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

  1. محمود سعداوي2

    محمود سعداوي2

    الأعضاء


    • نقاط

      5

    • المساهمات

      604


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

    عمر قره محمد

    الأعضاء


    • نقاط

      4

    • المساهمات

      4096


  3. محمد المصري12

    محمد المصري12

    الأعضاء


    • نقاط

      3

    • المساهمات

      276


  4. Ahmed Sadek Elamine Touahria

    • نقاط

      2

    • المساهمات

      510


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

المحتوى الأعلى تقييمًا في 11/20/22 في كل الموقع

  1. يمكنك من خلال index حلقة التكرار فعل هذا الأمر بحيث لو كان لديك الاستعلام سوف تقوم بعمل حلقة تكرار عليه لتقوم بجلب النتائج مثل <?php for($i=0; $i < count($sel_sub); $i++) { if($i == 11){ // هنا تقوم بجلب القيمة الخاصة بالنتيجة رقم 11 } }
    2 نقاط
  2. اريد انشاء الصفحة الرئيسية في دجانغو و وفيها زر تسجيل دخول الى برنامج moodle و ايضا عرض بعض الكورسات الموجودة في برنامج moodle في الصفحة الرئيسية المبنية في دجانغو هل هناك طريقة ربط بين دجانغو و moodle
    1 نقطة
  3. قمت بدالة أريد من خلالها إرجاع عنصر محدد (الذي قمت بالضغط عليه) المشكل أن النتيجة تكون دائما أول عنصر من المصفوفة const noteClicked = () => { setCreating(false) let selectedNote = notes.find((note,i)=> note.id === notes[i].id) console.log(selectedNote) } شكرا جزيلا.
    1 نقطة
  4. المشكلة ليست في الـ find ولكن في الشيء الذي تبحث عنه، حيث انك لا توفر الـ id الذي تبحث عنه في الوظيفة. وبالتالي فالـ filter ستعطيك مشكلة شبيهة بالسابق
    1 نقطة
  5. يحدث ذلك لأن الشرط الذي كتبته داخل الـ find : note.id === notes[i].id يكون صحيحاً من اجل اول عنصر في المصفوفة دائماً، ليس هذا فقط بل إنه صحيح لكل عناصر المصفوفة. ولأن الوظيفة find مصممة لتعيد اول عنصر يحقق الشرط فلذلك تعيد العنصر الأول دائماً. ولحل المشكلة يجب ان تحصل على الـ id بطريقة مختلفة، مثل إضافة الوظيفة التي تريدها عند انشاء العنصر وإضافة الـ id الخاص به لهذه الوظيفة كالتالي : Notes?.map(note => ( <dev onClick = {() => noteClicked(note.id) }> {note.text} </dev> )) const noteClicked = (id) => { setCreating(false) let selectedNote = notes.find((note,i)=> note.id === id) console.log(selectedNote) }
    1 نقطة
  6. نص الخطأ يوضح بأنك عند إنشاءك لمفاتيح token الخاصة ب google auth لم تقم بإضافة urls الخاصة ب test مثل localhost و 127.0.0.1 . لذلك يرجى الذهاب إلى https://console.developers.google.com/ ثم اختيار تطبيقك وقم بإضافة url التي تعمل عليها
    1 نقطة
  7. ازا كان لدي موجه من نوع get اول له متحكم خاص به ويقوم بعرض الطلاب في صفحة index. Blade. Php موجه اخر يقوم بجلب قيمة من form ادخال وله متحكم خاص ولكن العرض يتم index. Blade. Php ايّ موجهين لكن من نفس صفحة blade
    1 نقطة
  8. يمكنك استعمال حزمة moodle_api لخدمات الويب Moodle REST قم بتفعيل خدمات الويب انتقل إلى إدارة الموقع -> المكونات الإضافية -> خدمات الويب -> نظرة عامة واتبع التعليمات لتمكين خدمات الويب وإنشاء رمز ترخيص مميز. الإستعمال الوظيفة الرئيسية هي moodle_api.call (function_name، ** kwargs) التي تستدعي وظيفة moodle API باسم محدد. يتم تمرير الوسيطات إلى وظيفة API كوسيطات كلمات رئيسية على سبيل المثال courseids = [1،2،3] ، categories = [{'id': 1، 'name': 'Some name'}، ... تصفح إدارة الموقع -> المكونات الإضافية -> خدمات الويب -> وثائق واجهة برمجة التطبيقات للحصول على وسيطات محددة لوظائف واجهة برمجة التطبيقات الممكنة. مثال >>> import moodle_api >>> moodle_api.URL = "https://my.moodle.site" >>> moodle_api.KEY = "xxxxx (moodle secret token)"# مفتاح الخاص بك >>> course5 = moodle_api.call('core_course_get_contents', courseid=5) >>> course5[0].keys() dict_keys(['id', 'summary', 'name', 'visible', 'summaryformat', 'modules']) قائمة الدورات Class CourseList تعالج قائمة بجميع الدورات. بمجرد التهيئة ، يمكنك الحصول على الدورات حسب المعرف والرقم. >>> courses = moodle_api.CourseList() >>> courses.by_id[5] {'categoryid': 9, 'categorysortorder': 170009, 'completionnotify': 0, 'courseformatoptions': [{'name': 'numsections', 'value': 17}, ... >>> courses.by_idnumber['1234'] {'categoryid': 9, 'categorysortorder': 170009, 'completionnotify': 0, 'courseformatoptions': [{'name': 'numsections', 'value': 17}, ...
    1 نقطة
  9. سلام عليكم الان انا بعمل استعلام عادي جدا $sel_sub = $conn->query("SELECT p.id AS pID , p.pack_id, p.tender_id, p.item_name,s.total_volume AS tv FROM pack_items p LEFT JOIN suppling_details s ON s.pack_item = p.id WHERE p.tender_id = $tender "); نتائج الإستعلام ده 11 نتيجة مثلا النتائج دي عاوز ارتبها من 1 ل 11 و بعدين هاحط متغير مثلا بقيمة معينة ( موجودة ضمن ال 11 بيان اللي طالعين ) عاوز طريقة اخليه يروح عالمتغير ده و يبص عالاستعلام يدور علي قيمة مساوية لقيمة المتغير ده و يطبعلي الرقم اللي قدامه ( ترتيبة من ال 11)
    1 نقطة
  10. السلام عليكم. كيف يمكنني إصلاح الشيفرة التالية: function Notes({notes}) { return ( <div className='notes'> { if (notes.length !== 0) { notes.map(note => { return <Note key={note.id} title={note.title} content={note.content}/> }) } } <button className='add-btn'>+</button> </div> ) } للتوضيح: قمت بجلب notes من import React , {useState} from 'react' import './AddNoteForm.css'; import Alert from '../Alert/Alert' import Notes from '../Notes/Notes' function AddNoteForm() { const [title,setTitle] = useState('') const [content,setContent] = useState('') const [error,setError] = useState(false) const [notes,setNotes] = useState([]) const handleChange_title = (e) => { setTitle(e.target.value) } const handleChange_content = (e) => { setContent(e.target.value) } const saveNoteHandler = (e) =>{ e.preventDefault() if (title.length === 0 || content.length === 0) { setError(true) } else{ setError(false) const note = { id : new Date(), title : title, content : content } const updatedNotes = [...notes,note] setNotes(updatedNotes) } } return ( <form> <input className='title-content input' type='text' placeholder='title' value={title} onChange={handleChange_title} /> <textarea className='note-content input' type='text' placeholder='text' rows='8' value={content} onChange={handleChange_content} /> <input className='input-btn input' type='submit' value='Save' onClick = {saveNoteHandler} /> {error ? <Alert/> : ''} </form> ) } export default AddNoteForm شكرا
    1 نقطة
  11. نعم لن تواجه اي مشكلة بذلك، أتوقع ان شكل البيانات التي يتم حقنها الى كل من ملفي العرض index.blade.php مختلفتان، ولذلك فإن سيتم تصيير الصفحة في كل حالة بطريقة مختلفة عن الأخرى. فان كان الموجه الاول يعرض الصفحة كـ: return view('index' ,compact('data1')); و الموجه الثاني يعرضها كـ: return view('index', compact('data2')); فإنك ستحتاج بطريقة ما للتحقق من ما ان كان متغير ما ممررا لتصيير الصفحة: @if(isset($data1)) .// طريقة العرض الاولى @elseif(isset($data2)) .// طريقة العرض الثانية @endif ورغم امكانية تطبيق الفكرة الا أنه لا يعد تطبيقا جيدا للشيفرة النظيفة، فهو لا يحترم مبدأ فصل المهام seperating of concerns بالدرجة الأولى. بالاضافة الى مبدأ المسؤولية الواحدة single responsibility. اذ يمكنك على كل حال فصل الاختلاف فقط. وان كان هنالك تشابه بين طريقتي العرض فيمكنك الاحتفاظ بالتشابه في ملف مكون منفصل وتصيير مكونات الاختلاف في كل مرة.
    1 نقطة
  12. في المثال التالي عند النقر العنصر المحدد لا تتغير className الكود import React,{} from 'react' function Note({title,noteClicked,active}) { return ( <h2 className={`note ${active && "active"} `} onClick={noteClicked}> {title} </h2> ) } export default Note بالرغم أن كل شيء تمام في console Notes.js function Notes({notes}) { const noteClicked = (e) => { console.log('===================================='); console.log(e.currentTarget.classList); console.log('===================================='); } return ( <div className='notes'> { notes?.map(note => <Note key={note.id} title={note.title} content={note.content} noteClicked={noteClicked} /> ) } <button className='add-btn'>+</button> </div> ) } export default Notes صورة الشاشة
    1 نقطة
  13. هذا يحدث لأن المتغير active في الكود : <h2 className={`note ${active && "active"} `} onClick={noteClicked}> {title} </h2> حيث ان الكود : active && "active" // غير معرف active يصبح كالتالي لأم الـ undefined && "active" لحل المشكلة قم بوضع الوظيفة noteClicked داخل الملف الخاص بالمكون Note ونضيف كذلك الـ state المسماة active كالتالي : import React, { useState } from "react"; function Note({ title }) { const [active, setActive] = useState(); const noteClicked = () => { setActive(true); }; return ( <h2 className={`note ${active ? "active" : ""} `} onClick={noteClicked}> {title} </h2> ); } export default Note; وكذلك يفضل ان نغير الـ "active && "active إلى الشكل : active ? "active" : "" والذي يعني إذا كان active صحيحاً قم بإعادة "active" وإلا قم بإعادة "" (نص فارغ). وذلك لأن الشكل "active && "active يعيد كلاس بالشكل "note false" بينما الشكل الثاني يعيد "note " عندما يكون المتغير active غير صحيح.
    1 نقطة
  14. #include <bits/stdc++.h> using namespace std; const int N = 1e5 + 10; int a[N]; int pf[N]; // Driver code int main() { int n = 6; int a[] = { 3, 6, 2, 8, 9, 2 }; pf[0] = a[0]; for (int i = 1; i < n; i++) { pf[i] = pf[i - 1] + a[i]; // cout<<pf[i]; } int q = 4; vector<vector<int> > query = { { 2, 3 }, { 4, 6 }, { 1, 5 }, { 3, 6 } }; for (int i = 0; i < q; i++) { int l = query[i][0], r = query[i][1]; if (r > n || l < 1) { cout << "Please input in range 1 to " << n << endl; continue; } if (l == 1) cout << pf[r - 1] << endl; else cout << pf[r - 1] - pf[l - 2] << endl; } return 0; }
    1 نقطة
  15. السلام عليكم ابحث عن plugin لتحويل html الى pdf باستخدام angular
    1 نقطة
  16. استخدمتها لكن اذا سويت request للصورة من api ما تظهر في ملف pdf
    1 نقطة
  17. يمكنك استعمال moodle_api و التي توفر لك طريقة سهلة للتعامل مع ال moodle في البايثون و بالتالي في django، يمكن الاطلاع على المكتبة من هنا.
    1 نقطة
  18. ايه هي عناصر هيكل بناء المواقع بالترتيب يعني مثلا اول حاجه header وبعديها ايه وكدا عايزاهم كلهم بالترتيب مفيش اجابة
    1 نقطة
  19. وماهو ال landing سمعت عنه كثيرا
    1 نقطة
  20. يمكن في خلل في طريقة سؤالي تجعله غير مفهوم و اعتذر لذلك انا استاذي لا اعلم الرقم لكي اضعه كشرط ان تحقق اطبعلي النتيجة انا عندي اسم ( و الاسم ده موجود ضمن نتائج الاستعلام ) فقط اريد رقمه ليس اكثر و لا أقل <?php echo '<pre>'; echo 'رقم البند بطانية للبالغين هو' ; echo '</pre>'; ?> انا عاوز لما يلاقي في الاستعلام بند اسمه ( بطانية للبالغين ) يكتبلي ترتيبه في الاستعلام ، بس كده مش اني اكتب الرقم و اقوله اطبعلي الاسم المقابل ليه
    1 نقطة
  21. بشكل عام لا يوجد هيكلية معينة لبناء المواقع ، بحيث ممكن أن يحتوي الموقع على عدّة أقسام مختلفة ، لكن المتعارف عليه في البداية يتم وضع قسم header الذي ممكن أن يحتوي على القائمة الرئيسية وشعار الموقع أو معلومات أخرى ، وممكن ألا يحتوي على القائمة الرئيسية للموقع بيحث تكون القائمة بعد قسم header . ثم بعد ذلك يأتي قسم أو ممكن أن نسميه (جسم الصفحة) الذي يحتوي على الكثير من الأقسام المختلفة ويتم إضافة هذه الأقسام حسب الفكرة العامة للموقع إذا كان موقع إخباري أو رياضي أو تعليمي ، ... إلخ . ثم في نهاية الصفحة يوضع footer الذي يوضع به بعض Links أو بيانات التواصل وحقوق الملكية ، أو إضافة ملخص تعريفي عن الموقع ،..إلخ . ممكن أن يحتوي الموقع على شريط جانبي sidebar ممكن أن يحتوي على معلومات وإقتباسات أو قوائم وصور ،..إلخ . لو قمت بالبحث على محرك البحث جوجل website ui design سوف تظهر لك العديد من نتائج البحث ويمكنك أن تأخذ تغذية بصرية عن كيفية بناء هيكلية الموقع وما هي الأقسام التي يتم وضعها في الغالب .
    1 نقطة
  22. معلش سامحني اخي الفاضل لكي استوعب الفكرة الكود كالتالي <table> <tr> <td> الاسم </td> <td> الرقم </td> </tr> <?php $i = 0; while ($row = mysqli_fetch_assoc($sel_sub)) { $i++ ?> <tr> <td> <?php echo $i ?> </td> <td> <?php echo $row['item_name'] ?> </td> </tr> <?php } ?> </table> <?php echo '<pre>'; echo 'رقم البند بطانية للبالغين هو' ; echo '</pre>'; ?> و دي نتيجة الإستعلام انا عاوز لما يلاقي المتغير قيمته مثلا ( بطانية للبالغين) يروح يدور عليها ف الاستعلام و يطبع الرقم اللي قدامها ( من غير ما اعرض باقي النتائج) يعني بطانية بالغين رقمها 9 لو ستائر نوافذ يجيبلي 14 لو لحاف يجيبلي 12 بس ، من غير ما يظهرلي باقي النتائج ( يعني ينفذ الاستعلام في الخفاء و يقارن و يحطلي الرقم بس)
    1 نقطة
  23. لا نحل أسئلة الامتحانات في الاكاديمية, ولكن يمكننا مساعدتك في حلها, بمعنى أن تحاول حلها ومشاركتنا الكود الخاص بك واذا كان هناك مشكلة في الكود الذي كتبته نساعدك في حل المشكلة
    1 نقطة
  24. في الشيفرة المرفقة لا يوجد استخدام للمكون Notes تأكد من مكان استخدامه وطريقة تمرير تلك الخاصية، كما يرجى إرفاق الجزء من الشيفرة الخاص بذلك الاستخدام بدلًا من رابط خارجي
    1 نقطة
  25. يمكنك تعيين قيمة افتراضية للمتغير notes تكون مصفوفة فارغة في حال لا يوجد بيانات، من داخل المكون يمكنك ذلك كالتالي: <div className='notes'> { (notes || []).map(note => <Note key={note.id} title={note.title} content={note.content}/>) } ... </div>
    1 نقطة
  26. إذا استخدم الطريقة الأولى : { notes?.map(note => { return <Note key={note.id} title={note.title} content={note.content}/> }) }
    1 نقطة
  27. السلام عليكم. في الكود التالي ينبهني terminal إلى رسالة الخطأ التالية: Array.prototype.map() expects a value to be returned at the end of arrow function مع العلم أن الكود يعمل بشكل صحيح. الكود: import React,{useState} from 'react' import './Notes.css' import Note from './Note' function Notes() { const arr = ["A","B","C","D"] const [count,setCount] = useState(0) const handleAdd = () => { if (count < arr.length) { setCount(count+1) console.log(arr[count]); } } return ( <div className='notes'> { arr.map((el, i) => { if (i < count) return <Note title={el} key={i} i={i} />; }) } <button className='add-btn' onClick={handleAdd}>+</button> </div> ) } export default Notes //error message WARNING in [eslint] src\Notes\Notes.js Line 17:19: Array.prototype.map() expects a value to be returned at the end of arrow function array-callback-return webpack compiled with 1 warning شكرا لكم.
    1 نقطة
  28. اكيد يمكنك استعمالها ب فيو ركز معي: انت كفرونت اند بأي اطار عمل يجب عليك تعلم استيراد وتصدير المعلومات الى قواعد البيانات ف عمليتي تسجيل الدخول والخروج مثلا تحتاج الى استيراد وتصدير بيانات الى قواعد البيانات كيف سوف تصدر او تستورد : سيقوم الباك اند بإعطائك رابط ال Api وانت تقوم بالتعامل معه او تستطيع جلب Api جاهزة والعمل عليها يمكنك البحث عن Fetch Api والتعامل به في Vue
    1 نقطة
  29. لقد أُثبتت أهمية المساحة البيضاء علميًا في تسهيل القراءة وتعزيز الفهم. ويقصد بالمساحة البيضاء هنا، الفراغات الموجودة بين محتويات الصفحة. هناك العديد من المقالات المنظمة سهلة القراءة، لكن بالمقابل تفتقر مقالات كثيرة لهذه البينة المرئية الأساسية؛ إذًا كيف تحسن تجربة القراءة للمتابعين؟ إذا أردت الحفاظ على جاذبية موقعك ووضوحه وأناقته، فيجب أن تُدخل بعض المساحة البيضاء في كل صفحة ضمنه. لا يشترط أن يتفرد كل تصميم فنيًا، بل يكفي وضع المساحة في المكان المناسب ليصبح التصميم جذابًا. سنتعرف تاليًا على العناصر الأساسية للمساحة البيضاء White Space المعروفة أيضًا باسم المساحة السلبية Negative space، بالإضافة لكيفية استخدامها لإضافة الجمال والكفاءة على التصميمات الإلكترونية. لا يفهم كثير من المصممين المقصود بالمساحة البيضاء ولا سبب أهميتها، لذا سنتعرف في هذا المقال على المقصود بالمساحة البيضاء وكيفية استخدامها بكفاءة، إلى جانب توضيح أهميتها. 1. اجعل إضافة المساحة البيضاء عادة لا شك أن الغالبية لا تهتم بتصميم المواقع، أو لا تهتم بتفاصيلها على الأقل. ولا نتحدث هنا عن الذين يتعمّدون إهمال تصميم مواقعهم، بل عن الذين يصممونها بعشوائية دون تدريب، أو بعد تلقيهم تدريبًا غير كافٍ، حيث يمكن أن يؤثر تصميم الموقع على معدل التحويل Conversion rates، وبالتالي على الأرباح الصافية. تُهمل في العادة بعض العناصر الأساسية أثناء تصميم المواقع رغم أهميتها، إذ يُعَد شعار الموقع مثلًا أحد أول العناصر التي يلاحظها مصممو المواقع، مع ذلك قد يُهمل في أحيان كثيرة، وهنا قد يؤدي التصميم السيء للشعار إلى إرباك زوار الموقع ويدفعهم إلى الخروج من الموقع بالكامل، بالتالي قد يكلفك وجود تصميم رديء للشعار خسائر مادية عالية. وتوضح الصورة السابقة الصفحة الرئيسية لموقع وكيف يوجد الشعار عادةً أعلى شريط التنقل. العنصر الثاني المهم في تصميم المواقع هو الخطوط المستخدمة ضمن الموقع، فعندما لا تكون الخطوط المستخدمة سهلة القراءة، ستصعب قراءة النصوص، كما سيبدو الموقع غير احترافي؛ وهنا يجب على مصممي المواقع التحقق من حجم الخط ضمن الموقع، والتأكد من أنه كبير بما يكفي ليُقرأ بسهولة، إضافةً للتأكد من الألوان المستخدمة في الخلفية. تسهّل الملاحظات السابقة قراءة النصوص ضمن الموقع. يعتقد البعض أن اختيار لون الخلفية غير مهم، لكنه في الواقع يلعب دورًا أساسيًا في تميز الموقع. 2. انظر إلى الميزات قد تبدو بعض المواقع الإلكترونية رائعةً بوجود مساحة بيضاء قليلة، بينما تبدو مواقع أخرى أفضل عند زيادة المساحة البيضاء؛ لذا إن كنت تفكر بالتعديل، فخذ الميزات التالية بالحسبان: قد تساعد المساحة البيضاء في إيضاح الموقع الإلكتروني وتمنحه طابعًا هادئًا وجماليًا وتسهل إدراك الصفحة على عين المستخدم، وهي العناصر التي تساعد الزوار في الحصول على تجربة ممتعة ضمن الموقع. تبدو معظم الصفحات أجمل عند إضافة المساحة البيضاء، لذا من الجيد استخدام المساحة لتعزيز الهدوء والجمالية. تأكد من وجود مساحة بيضاء كافية ضمن موقعك، إذ تساعدك المساحة البيضاء على تحديد المحتوى ضمن الصفحة، كما تمنحه طابعًا هادئًا، وبالتالي زيادة أناقة الصفحة وتسهيل قراءتها. تحوي العديد من المواقع على المساحات، لكن وجود المساحة لا يعني أنها وُضعت بكفاءة. كذلك، تحوي بعض المواقع فراغات كثيرة، بينما لا تستخدم مواقع أخرى مساحة بيضاء كافية، لذا عليك استخدام المساحة بحكمة. إذا بالغت في استخدام المساحة البيضاء، فسوف يمل زوار الموقع بسرعة ويغادرونه، أما إذا لم تستخدمها بما يكفي، فستصعب قراءة المحتوى على الزوار. تذكر الفكرة السابقة أثناء تصفحك المواقع، وتأكد من أن الصفحات لا تبدو مزدحمة. 3. استخدم المساحة البيضاء بأسلوب صحيح من غير المنطقي أن تصمم بدون أي مساحة بيضاء، وذلك لأن المساحة تساعد العين في التركيز على المحتوى. حافظ على تناسق المساحة البيضاء ضمن موقعك الإلكتروني لتحقيق التأثير المرغوب، واستخدم حجمًا موحدًا للخطوط لكل من العناوين والفقرات والنصوص الأخرى، وتأكد من وجود هامش 20 بكسل على الأقل بين العناصر. يتساءل المطورون عادةً حول استخدام المساحة البيضاء بكثرة أو عدم استخدامها مطلقًا. وأحد الأسباب الذي قد يدفعنا للتصميم دون استخدام مساحة بيضاء، هو جذب انتباه المستخدم إلى محتوى الصفحة وتوجيهه إلى التركيز عليه. تُعَد هذه الفكرة جيدة لأننا نعلم أن استيعابنا للمعلومات يتحسن عندما تزداد مدخلاتها البصرية، لذا إذا كنا نحاول جذب المستخدم، فعلينا تجنب المبالغة في التركيز على العناصر والاهتمام بتصميم موقع جذاب وسهل القراءة. إذا كانت العناصر تشتت انتباه المستخدم عن المحتوى، فإنهم لن يكرّسوا وقتًا كافيًا لقراءته، وقد لا يعمل الموقع على النحو المرغوب. وعندما ننشئ مواقع إلكترونيةً بدون مساحة بيضاء، فإننا نحاول تركيز انتباه المستخدم على المحتوى، لذا يجب علينا تجنّب المشتتات بين القارئ والمحتوى لأن هدفنا الأساسي هو جذب انتباه زوار الموقع. 4. لا تبالغ في استخدام المساحة البيضاء المساحة البيضاء صديقتنا. يجب على المصممين التأكد من الجاذبية البصرية للتصميم ومعرفة أن الاهتمام بالمساحة البيضاء ليس إهدارًا للوقت. من المهم جدًا الحفاظ على كفاءة التصميم، وإذا أردت أن تستخدم المساحة البيضاء بكفاءة، فيجب أن تفهم كيفية استخدامها أولًا. يجب أن تستخدم المساحة البيضاء ضمن المخطط Layout لتعزيز جاذبيته، وهناك أساليب مختلفة لتطبيق المساحة البيضاء. يمكنك مثلًا استخدام المساحة السلبية Negative Space، وهي المساحة الموجودة بين عنصرين أو خطين، حيث يمكنك استخدام المساحة السلبية للإيحاء بالحركة والانسيابية. يمكنك استخدامها أيضًا لفصل العناصر ضمن صفحة، ولإنشاء توازن بصري وتعزيز الجمالية، أو للإيحاء بالعمق والحركة، ولتعزيز الوضوح؛ وتعد أسلوبًا مناسبًا جدًا ليتميز تصميمك عن الآخرين. يُعَد العنوان أحد أفضل الأماكن لإدراج المساحة البيضاء، لذا من الضروري التأكد من أن العنوان مميز وسهل التذكر، إذ يحسّن إدراج المساحة البيضاء حول العنوان معدل التحويل (معدل تحويل زوار الموقع إلى عملاء)؛ أما إذا بالغت في إضافة المساحة البيضاء، فقد تخسر بعض القراء غير المهتمين بالعنوان، وبالمقابل قد لا يتمكن القرّاء من قراءة العنوان إذا لم تستخدم مساحةً بيضاء كافية. 5. جرب استخدام المساحة البيضاء لا تستفيد معظم المواقع من كامل ميزات المساحة البيضاء. تعني المساحة البيضاء الفراغ ببساطة، ونراها دومًا في المجلات والكتب واللوحات الإعلانية والتطبيقات، وهي فرصة ممتازة حتى تضيف الشركات إثارةً بصريةً على موقعها أو مدونتها. يوجد سببان لاستخدام المساحة البيضاء كما هو الحال في المجلات، أحدها هو توكيد الكلمات أو تعزيز الأفكار، والسبب الآخر هو الإيحاء بالحيوية والتشويق. رغم ميزات المساحة البيضاء، إلا أنه يجب تجنب المبالغة بالاهتمام بها على حساب المحتوى، وذلك حتى لا تشتت القارئ عن الرسالة الأساسية. فقد يعني وجود المساحة البيضاء ضمن موقع إلكتروني أن مصمم الموقع لم يفهم غايتها ولم يفكر بإمكانية تأثيرها على الموقع وبالغ باستخدام المساحة البيضاء دون مبرر، لذا يمكنك اختزال المساحة البيضاء بسرعة دون تحويل الموقع إلى فوضى بحذفها وإضافة أحد العناصر التالية: صورة عنوان عنوان فرعي نص مخطط إذا قررت إضافة صور، فتأكد من اختيار الحجم المناسب، مع اختيار حجم مناسب لمحتوى الموقع أو المدونة. تأكد أثناء اختيار حجم الصورة من أخذ الأفكار التالية بالحسبان: يجب أن يكون الحجم ملائمًا للموقع أو المدونة. تجنب وضع الشعار في منتصف الصفحة إذا كان كبيرًا. يجب أن تكون الصورة أصغر من المربع النصي. تُعَد الأشرطة الإعلانية Banners مثالًا ممتازًا عن الصور التي يجب أن تكون أصغر من النص. يجب استخدام الصور لتحسين محتوى الصفحة، وهذا يعني استخدام الصورة لإبراز فكرة أو عبارة معينة. لا يشترط أن تستخدم صورة معينة لمجرد توفرها. يجب أن تكون الصورة كبيرةً كفايةً لقراءتها والنقر عليها، خاصةً إذا كنت ترغب في زيادة حركة مرور الموقع. وعند وجود شريط إعلاني، فإنه يجب أن يكون أكبر من الصور. 6. استخدم المساحة البيضاء كعادة تسويقية وفقًا لجيمس سميث James Smith، هناك أدلة كثيرة عن أهمية وجود المساحة البيضاء حول نص في جذب الانتباه إليه. على سبيل المثال، إذا كان العنوان: "أنا أفقد وزني!"، فقد لا يرغب زوار الموقع بقراءته، لكن إذا غيرت العنوان ليصبح: "سبب فقدي للوزن هو شرب مخفوق البروتين التالي مرتين يوميًا"، فقد يظن زوار الموقع أنه رائع. تثبت هذه التجربة الصغيرة تأثير المساحة البيضاء. قد تكون إضافة بضع فقرات إلى مقالات مدونتك فكرةً رائعة. وللفقرات ثلاث أنواع: الفقرة القصيرة: تستخدم عند نشر بعض التفاصيل عن موضوع مقالات المدونة. الفقرة الطويلة: توسّع معرفة القارئ عن الموضوع وتعزز فهمه. الفقرة الموسعة: تكون وسطًا بين الفقرات القصيرة والطويلة، وتُعَد جيدةً عند الكتابة عن موضوع لا يكفيه الوصف المختصر. 7. أبدع في استخدام المساحة البيضاء يُعَد جذب انتباه المستخدم إلى عنصر محدد ضمن الموقع أحد أفضل أساليب استخدام المساحة البيضاء، حيث يمكنك تطبيق الفكرة السابقة باستخدام العناصر المرئية مثل الأزرار الكبيرة الملونة والزاهية التي تجذب انتباه المستخدم وتشجعه على نقرها، أو المساحة البيضاء بين محتوى الموقع لإضفاء الإثارة والغموض. وعندما تبدأ بإنشاء موقعك، عليك أولًا البحث عن أساليب لإدخال الإثارة والغموض إليه، وذلك لأن الغموض يجذب الزوار إلى الموقع. يمكنك زيادة تجاوب الموقع بإضافة عناصر واجهة مستخدم رسومية جذابة Widgets، أو ألعاب، أو عناصر أخرى تحوّل زيارة الموقع إلى تجربة ممتعة. 8. استفد من المساحة البيضاء على الإنترنت لا تهدف المساحة البيضاء إلى تجنب الإعلانات النصية فحسب، لذا خذ أسلوب تنسيق نسختك الإلكترونية بالحسبان، ومظهر الموقع الإلكتروني، وكيف يبدو شعار شركة ما، وما المميز فيه؛ سواءً كان شعارًا واضحًا وبارزًا، أو بسيطًا وأنيقًا، وذلك دون إضافات غير ضرورية، وتذكر في كل مرة تزور فيها موقعًا إلكترونيًا أنك تشاهد الهوية البصرية للشركة. التفكير وفق الأسلوب السابق ضروري لأنه يؤثر على كل ما تراه. وتتمثل الخطوة الأولى في التأكد من فهمك لعلامتك التجارية، فحين تعرف علامتك التجارية، يأتي دور النظر في الهوية البصرية العامة، وعندها تكون قد أنشأت لغتك المرئية. على سبيل المثال، عندما تستخدم نظام ألوان بدرجات اللون الأخضر، فعليك تجنب استخدام الألوان الأخرى ضمن موقعك حتى لا تربك القرّاء. تجنب أيضًا استخدام خطوط عديدة ضمن الصفحة، واختر خطًا واحدًا، ويفضل أن يكون أحد خطوط Sans Serif لأنه خط مألوف للغالبية، كما أنه متباين جيدًا، وبالتالي يسهّل من قراءة نسختك. الأمر التالي هو التأكد من امتلاكك تنسيقًا جيدًا، ومن وجود مساحة بيضاء كافية لتسهيل قراءة المحتوى وفهمه، إذ يصعب إيجاد أي عنصر ضمن موقعك عندما يكون فوضويًا. 9. استخدم المساحة البيضاء ضمن مواقع التواصل الاجتماعي كثيرًا ما يتجاهل الفنانون ومصممو الجرافيك المساحة البيضاء، وذلك دون إدراك لأهميتها كعنصر أساسي في الفن الحديث. وتُعَد المساحة البيضاء عنصرًا أساسيًا في نجاح بعض المواقع مثل Pinterest وTumblr وInstagram وFacebook. ركزت التوجهات في العقد الماضي على مبدأ "الأقل هو الأكثر" Less is more ، وأدركت الشبكات الاجتماعية أن المساحة البيضاء تُعَد حلًّا رائعًا لإحياء تصميم رتيب، كما أنها تسهّل تصفح الإنترنت والموجزات الاجتماعية Social Feeds. عندما تستخدم المساحة البيضاء بكفاءة، تجب مراعاة نوع المعلومات التي ترغب بمشاركتها مع المعنيين. فهل ترغب بمشاركة صور للشاطئ؟ أم مشاركة روابط مقالات أخرى؟ إذا كان الأمر كذلك، فمن الأفضل استخدام المساحة البيضاء للحفاظ على الوضوح. إذا أردت استخدام المساحة البيضاء لإخبار المعنيين أنك سترفق بعض المعلومات، فيُفضّل إضافة نص فوق الصورة ليعلموا أن الصورة ليست محور الاهتمام. 10. طبق المساحة البيضاء لا تُعَد المساحة البيضاء عنصرًا حديثًا، فهي موجودة منذ بداية عهد وسائل الإعلام المطبوعة، وما تزال مستخدمةً حتى الآن، لكن هناك علامات تجارية كثيرة لم تعتمد هذه الممارسة بعد. لقد كانت المساحة البيضاء سابقًا أبسط من أن تدخل في حملات العلامة التجارية، لكن هذا المفهوم قد تغير سريعًا وفقًا لكوبس Cupps، إذ يتعلم المستهلكون أن التصميم المرئي هو المنتج الحقيقي، ويمكنهم إنشاء نسختهم منه والبحث عن عناصر التصميم التي تراعيهم بدل العناصر المصممة، وذلك دون أخذ المستهلكين بالحسبان". يعتقد المصممون والمسوقون أن استخدام المساحة البيضاء وسيلة ممتازة لإبراز علامة الشركة التجارية. يركز المستهلكون على الشعار أكثر من أي شيء آخر، وتعزز المساحة البيضاء جذب انتباه المستهلكين إلى الشعار واسم العلامة التجارية. يجب أن يركز المصممون والمسوقون على العناصر التي تخاطب المستهلك، وتُعَد المساحة البيضاء عنصرًا إبداعيًا يخاطب المستخدم، وكذلك عنصر اللون؛ كما أن الرسومات ومقاطع الفيديو عناصر أخرى يجب أن تؤخذ بالحسبان. إذا استخدمت شركة ما المساحة البيضاء، فإنها ستجذب انتباه المستخدمين أكثر من التصميمات الأخرى. تُعَد المساحة البيضاء أهم العناصر التصميمية هنا، لأن المستهلكين يلاحظونها بوتيرة أكبر، كما تجذب العناصر الأخرى انتباه المستهلكين مثل الشعارات والخطوط. سيزيد استخدام الشركة لشعار أو خط موافق لعلامتها التجارية من جذب انتباه المستخدمين، وسيرغب المستهلكون بشراء منتج الشركة التي تملك شعارًا رائعًا، ولن يرغبوا بشراء منتج الشركة ذات الشعار الرديء. يجب أن يملك قسم التسويق للشركة عناصر تصميميةً ملائمةً لعلامتها التجارية، ويجب أن تحوي حملةً تسويقيةً لشركة معنية بالتقنيات الفائقة على عناصر تقنية مناسبة؛ بينما لن يحتاج مطعم للوجبات السريعة إلى إضافة عناصر تقنية إلى إعلاناته وعناصره التسويقية، حيث إن العناصر التقنية مفيدة فقط لشركات التقنيات الفائقة. يجب أن يتجنب المصممون والمسوقون العناصر التصميمية التي لا تطابق علاماتهم التجارية في إعلاناتهم، وذلك لأن المستهلكين يتجاهلونها. كذلك، يجب أن يُنشئ المصممون والمسوقون عناصرهم التصميمية مع مراعاة العلامة التجارية، ويجب أن يوظّفوا مصممي رسوميات Graphic Designers لإنجاز هذه المهمة. 11. أطلق العنان لإبداعك يُعَد التباعد Spacing والهوامش Margins والمحاذاة Alignment وغيرها، أخطاءً شائعةً قد تحمل عواقب كارثية؛ وقد تصادف أن مفهوم قاعدة الأثلاث Rule Of Thirds موجود على مواقع التواصل الاجتماعي مثل انستغرام وفيسبوك. تنطبق هذه القاعدة على موقعك الإلكتروني أيضًا، لذا احذر وتجنب أي أخطاء تصميمية قد تؤذي علامتك التجارية، حيث يمكن أن يسبب أبسط خطأ في المحاذاة ضمن صفحة إلكترونية مشكلة حقيقية. يجب أن يملك الموقع الهيئة والشعور المناسبين، أي يجب أن تستخدم الألوان والخطوط المناسبة وتنسيقًا مصممًا بإتقان. لن تحتاج شعارًا غريبًا هنا، إذ يكفي أن يكون فريدًا وسهل التذكر. لست بحاجة لإنفاق الكثير على تصميم الموقع، حيث توجد حلول عديدة لإنشاء موقع رائع بسعر مقبول. فكر بالاستعانة بمصمم مستقل أو فنان رسوميات لإنشاء صفحة بسيطة. وإذا كنت لا تملك موقعًا إلكترونيًا، فإنك تتجاهل أداة عمل رائجة ومتاحة. خاتمة تُعَد المساحة البيضاء إحدى أكثر جوانب التصميم تجاهلًا. ورغم كونها مساحةً فارغة، إلا أن وجودها مهم. لربما صادفت مصطلح المساحة البيضاء في كتاب عن قواعد التصميم أو ما شابه، لكن هل أدركت أهميتها؟ تفصل المساحة البيضاء بين عناصر التصميم وتركز عليها وتمنحها توازنًا بصريًا، حيث يمكن أن تفقد رسالة التصميم الأساسية بسهولة بدون المساحة البيضاء، خاصةً عند وجود الصور والنصوص بجانب بعضها. تُعَد المساحة البيضاء سلاحًا سريًّا لتطوير شركتك أو حياتك المهنية، لذا لا تتجاهلها رجاءً. ترجمة -وبتصرّف- للمقال White Space: Why It’s Important & How To Use It. اقرأ أيضًا لماذا تعتبر المساحات البيضاء مهمة في عالم التصميم كيف تستفيد من المساحات البيضاء في تعزيز نسبة التحول في صفحتك أهمية الفراغات البيضاء في تصميم الويب أساسيات التصميم المرئي
    1 نقطة
  30. استخدمنا تقنية البرمجة على التوازي بمثال القسم الفرعي التعاود داخل الخيوط من المقال السابق البرمجة باستخدام الخيوط threads في جافا لتنفيذ أجزاءٍ صغيرة من مهمةٍ كبيرة، حيث تَسمَح تلك التقنية للحواسيب مُتعدّدة المعالجات بإكمال عملية المعالجة بوتيرةٍ أسرع. ولكننا في الواقع لم نَستخدِم لا الطريقة الأفضل لتقسيم المشكلة ولا الطريقة الأفضل لإدارة الخيوط threads. سنتناول بهذا القسم نسختين من نفس البرنامج؛ حيث سنُحسِّن بالنسخة الأولى طريقة تقسيم المشكلة إلى عدّة مهام؛ بينما سنُحسِّن بالنسخة الثانية طريقة اِستخدام الخيوط. بالإضافة إلى ذلك، سنمرّ عبر مجموعةٍ من الأصناف المبنية مسبقًا والتي تُوفِّرها جافا لدعم المعالجة المتوازية، وسنناقش بنهاية القسم التابعين wait()‎ و notify()‎ المُستخدمين للتحكُّم بالعمليات المتوازية تحكمًا مباشرًا. تقسيم المشكلات يُقسِّم البرنامج MultiprocessingDemo1.java مُهمة حساب صورة إلى عدة مهامٍ فرعية، ويُسنِد كُلًا منها إلى خيط. يَعمَل هذا البرنامج بنجاح، ولكن هناك مشكلة متمثّلة في إمكانية استغراق بعض المهام وقتًا أطول من المهام الأخرى. في حين يُقسِّم البرنامج الصورة إلى أجزاءٍ متساوية، تتطلَّب بعض أجزائها معالجةً أكثر من أجزاءٍ أخرى. إذا شغَّلت البرنامج باستخدام ثلاثة خيوط، ستلاحظ أن معالجة الجزء الأوسط تستغرِق وقتًا أطول بعض الشيء من معالجة الجزأين السفلي والعلوي. في العموم، عندما نُقسِّم مشكلةً معينةً إلى عدة مشكلاتٍ فرعيةٍ أصغر، يكون من الصعب توقُّع الزمن الذي ستحتاجه معالجة كل مشكلةٍ منها. والآن، لنفترض أن لدينا مشكلةً فرعيةً معينة تستغرق زمنًا أطول من غيرها. في تلك الحالة، ستكتمل جميع الخيوط عدا الخيط المسؤول عن معالجة تلك المشكلة؛ حيث سيستمر بالعمل لفترةٍ طويلةٍ نسبيًا، وسيَعمَل معالجٌ واحدٌ فقط خلال تلك الفترة دون بقية المعالجات. إذا احتوى الحاسوب مثلًا على معالجين، وقسَّمنا مشكلةً ما إلى مشكلتين فرعيتين، ثم أنشأنا خيطًا لكل مشكلةٍ منهما. نطمح باستخدامنا لمعالجيّن إلى الحصول على الإجابة بنصف الزمن الذي سيستغرقه الحصول عليها عند استخدام معالجٍ واحد، ولكن إذا كان حل مشكلةٍ منهما يحتاج إلى أربعة أضعاف الزمن الذي يحتاجه حل المشكلة الأخرى، فسيكون معالجًا واحدًا فقط مُشغَّلًا غالبية الوقت، ولم نُقلِل في تلك الحالة الزمن المطلوب لحل المشكلة بنسبةٍ أكبر من 20%. حتى لو تمكَّنا من تقسيم المشكلة إلى عدة مشكلاتٍ فرعية تتطلَّب زمن المعالجة نفسه، فإننا لا نستطيع الاعتماد ببساطةٍ على حقيقة كونها ستتطلَّب زمنًا متساويًا؛ حيث من الممكن أن تكون بعض المعالجات منهمكةً بتشغيل برامجٍ أخرى، أو قد يكون بعضها أبطأ عمومًا (هذا ليس محتملًا إذا شغَّلنا البرنامج على حاسوبٍ واحد، ولكن تختلف سرعة المعالجات بالحوسبة المُوزَّعة من خلال مجموعة حواسيب عبر الشبكة -وهو ما سنفعله لاحقًا-، وتُعدّ عندها مسألةً مهمة). تتمثّل التقنية الأكثر شيوعًا للتعامل مع كل تلك المشكلات في تقسيم المشكلة إلى عددٍ كبيرٍ من المشكلات الفرعية الأصغر؛ أي أكبر بكثير من عدد المعالجات الموجودة، وسيضطّر بالتالي كل معالجٍ لحلّ عدة مشكلاتٍ فرعية. عندما يُكمِل معالج مهمةً فرعيةً معينة، تُسنَد إليه مهمةٌ فرعيةٌ أخرى ليَعمَل عليها إلى أن تُسنَد جميع المهام الفرعية. سيظل بالطبع هناك اختلافٌ بالزمن الذي تتطلَّبه كل مهمة، وبالتالي قد يُكمِل معالجٌ معينٌ عدة مشكلاتٍ فرعية، بينما قد يَعمَل معالجٌ آخر على مهمةٍ واحدةٍ معقدة؛ كما قد يُكمِل معالج بطيءٌ أو مشغولٌ مهمةً واحدةً فقط أو اثنتين، بينما قد يُنهِي معالجٌ آخر خمس أو ست مهام. في العموم، سيَعمَل كل معالجٍ وفقًا لسرعته الخاصة، وستستمر غالبية المعالجات بالعمل إلى قرب اكتمال المعالجة بالكامل بشرط أن تكون المشكلات الفرعية صغيرةً كفاية. ويُطلَق على ذلك "توزيع الحمل load balancing"؛ أي توزيع حمل المعالجة بين المعالجات المتاحة لإبقائها جميعًا منشغلةً بأقصى ما يُمكِن. ستنهي بعض المعالجات بالطبع عملها قبل بعضها الآخر، ولكن بفترةٍ لا تتعدى الزمن المطلوب لإتمام المهمة الفرعية الأطول. كما ذكرنا بالأعلى، ينبغي أن يكون حجم المشكلات الفرعية صغيرًا لا غاية في الصغر؛ لأن تقسيم المشكلة وإسنادها إلى المعالجات يتطلَّب حملًا إضافيًا overhead؛ فإذا كانت المشكلات الفرعية غايةً في الصغر، سيتراكم الحمل الإضافي ويُشكِّل فارقًا بمقدار العمل الكلي المطلوب. كانت المهمة بالمثال السابق هي حساب لون كل بكسلٍ بالصورة، فإذا أردنا تقسيم تلك المهمة إلى عدة مهامٍ فرعية، هناك عدة احتمالات؛ فقد تتكوَّن كل مهمة فرعية مثلًا من حساب لون بكسلٍ واحدٍ فقط، ولكنها تُعدّ صغيرة للغاية. بدلًا من ذلك، ستتكوَّن كل مهمةٍ فرعية من حساب لون صفٍ واحد من البكسلات. نظرًا لوجود عدة مئاتٍ من الصفوف بكل صورة، سيكون عدد المهام الفرعية كبيرًا كفاية، كما سيكون حجم كل مهمةٍ فرعيةٍ منها مناسبًا. سينتج عن ذلك توزيعًا جيدًا للحمل مع قدرٍ معقولٍ من الحمل الإضافي. ملاحظة: تُعدّ المشكلة التي ناقشناها بالمثال السابق سهلةً للغاية بالنسبة للبرمجة على التوازي؛ بمعنى أنه عند تقسيم مشكلة حساب ألوان الصورة إلى عدة مشكلاتٍ فرعية أصغر، ستَظِل جميع المشكلات الفرعية مستقلةً تمامًا، وبالتالي يُمكِن معالجة أي عددٍ منها بنفس الوقت ووفقًا لأي ترتيب. في المقابل، قد تتطلّب بعض المهام الفرعية بمشكلاتٍ أخرى النتائج المحسوبة بواسطة بعض المهام الأخرى؛ أي أن المهام الفرعية غير مستقلة، وبالتالي ستتعقد الأمور في تلك الحالات، وسيُصبح ترتيب تنفيذ المهام الفرعية مُهمًا. علاوةً على ذلك، سيكون من الضروري توفير طريقةٍ ما لتشارُك تلك النتائج بين المهام؛ وفي حالة تنفيذها بخيوطٍ مختلفة، سنضطّر إلى مواجهة كل تلك المشكلات المُتعلّقة بالتحكم بوصول الخيوط إلى الموارد التشاركية. لذلك، يُعدّ تقسيم مشكلةٍ معينةٍ لمعالجتها على التوازي أمرًا أصعب بكثير مما قد يوحي به المثال السابق، ولكن بالنهاية، يُعدّ ذلك موضوعًا لدورةٍ تدريبية عن الحوسبة المتوازية لا دورةٍ عن أساسيات البرمجة. مجمع الخيوط وأرتال المهام بعد أن نُقرِّر طريقة تقسيم المشكلة إلى عدة مهامٍ فرعية، ينبغي أن نعرف كيفية إسناد تلك المهام الفرعية إلى الخيوط؛ حيث ينبغي وفقًا للأسلوب كائني التوجه object-oriented تمثيل كل مهمةٍ فرعيةٍ بواسطة كائن، ولأن كل مهمة تُمثِّل عملية معالجة معينة، فمن البديهي أن يُعرِّف ذلك الكائن تابع نسخة instance method يُنفِّذ تلك العملية. من الضروري استدعاء التابع المقابل لمهمةٍ معينة حتى تُنفَّذ، وسيكون تابع المعالجة لهذا البرنامج، هو run()‎، وسيُنفِّذ الكائن المُمثِّل للمهمة الواجهة interface القياسية Runnable التي ناقشناها بقسم إنشاء الخيوط وتشغيلها من مقال مقدمة إلى الخيوط Threads في جافا. تُعدّ تلك الواجهة الطريقة المباشرة لتمثيل مهام المعالجة، ويُمكِننا إنشاء خيطٍ جديدٍ لكل كائن Runnable، ولكن في حالة وجود عددٍ كبيرٍ من المهام، لا يكون لذلك أي معنى؛ نتيجةً لمقدار الحمل الإضافي الناتج عن إنشاء كل خيطٍ جديد. بدلًا من ذلك، يُفضَّل إنشاء عددٍ قليل من الخيوط، بحيث يُسمَح لكُلٍ منها بتنفيذ قدرٍ معيّنٍ من المهام. لاحِظ أن عدد الخيوط التي ينبغي استخدامها غير معروف، وقد يعتمد على المشكلة التي نحاول حلها. يتمحور الهدف عمومًا في إبقاء جميع معالجات الحاسوب مُشغَّلة؛ فبالنسبة لمثال حساب الصورة، كان إنشاء خيطٍ مقابل كل معالجٍ مناسبًا، ولكنه قد لا يتناسب مع جميع المشكلات. وبالتحديد، إذا كان هناك خيطٌ قد يتسبَّب بحدوث تعطيلٍ block لفترةٍ طويلة بينما ينتظر وقوع حدثٍ event معين، أو بينما ينتظر الوصول إلى موردٍ ما، فلربما يكون من الأفضل إنشاء خيوطٍ إضافية؛ لكي يَعمَل عليها كل معالجٍ أثناء تعطُّل الخيوط الأخرى. يُطلَق على مجموعة الخيوط المُتاحة لتنفيذ المهام اسم "مجمع الخيوط thread pool"، وتُستخدَم بغرض تجنُّب إنشاء خيطٍ جديدٍ لكل مهمة، حيث تُسنَد المهمة المطلوب تنفيذها إلى أي خيطٍ مُتاحٍ بالمجمع. عندما تنشغِل جميع الخيوط الموجودة بالمجمع، تضطّر أي مهامٍ أخرى إضافية للانتظار إلى أن تُتاح إحدى الخيوط، ويُعدّ ذلك تطبيقًا مباشرًا على الرتل queue؛ حيث يرتبط بمجمع الخيوط رتلٌ مُكوَّنٌ من المهام قيد الانتظار. بمجرد توفُّر مهمةٍ جديدة، ستُضَاف إلى الرتل؛ وبمجرد انتهاء خيطٍ معينٍ من تنفيذ المهمة المُوكَلة إليه، فسيَحصُل على مهمةٍ أخرى من الرتل ليَعمَل عليها. هناك رتل مهامٍ task queue وحيدٍ لمجمع الخيوط. يَعنِي ذلك استخدام جميع خيوط المجمع نفس الرتل، فهو يُعدّ موردًا تشاركيًا. كما هو الحال مع أي موردٍ تشاركي، قد تقع حالات التسابق race conditions، ولهذا يكون استخدام المزامنة synchronization ضروريًا؛ فقد يحاول بدونها خيطان قراءة عنصرٍ من الرتل بنفس الوقت مثلًا، وعندها سيَحصُل كلاهما على نفس العنصر. حاول التعرف على الأماكن المُحتمَلة لوقوع حالات التسابق بالتابع dequeue()‎ المُعرَّف في قسم الأرتال Queues من الفصل المكدس Stack والرتل Queue وأنواع البيانات المجردة ADT. تُوفِّر جافا الصنف ConcurrentLinkedQueue من أجل حل تلك المشكلة؛ وهو صنفٌ مُعرَّفٌ بحزمة package java.util.concurrent إلى جانب أصنافٍ أخرى مفيدة للبرمجة على التوازي. لاحِظ أنه صنف ذو معاملاتٍ غير مُحدَّدة النوع parameterized، ولذلك إذا أردنا إنشاء رتلٍ لحَمْل كائناتٍ من النوع Runnable، يُمكِننا كتابة ما يلي: ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>(); يُمثِّل هذا الصنف رتلًا مُنفَّذًا مثل قائمةٍ مترابطة linked list، كما أن عملياته متزامنةً بطريقةٍ مناسبة. ليست العمليات المُعرَّفة بالصنف ConcurrentLinkedQueue نفس العمليات على الأرتال التي عهدناها؛ فعلى سبيل المثال، يُضيف التابع queue.add(x)‎ العنصر الجديد x إلى نهاية queue؛ بينما يَحذِف التابع queue.poll()‎ عنصرًا من مقدمة الرتل queue. إذا كان الرتل فارغًا، يعيد التابع queue.poll()‎ القيمة null، ولهذا يُمكِننا استخدامه لفحص فيما إذا كان الرتل فارغًا، أو لإسترجاع عنصرٍ إذا لم يكن كذلك. في الحقيقة، يُفضَّل فعل ذلك على هذا النحو؛ فقد يؤدي التأكُّد مما إذا كان الرتل فارغًا قبل قراءة عنصرٍ منه إلى وقوع حالة تسابق؛ حيث يستطيع خيطٌ آخر بدون تحقيق المزامنة حذف آخر عنصرٍ بالرتل باللحظة الواقعة بين لحظتي اختبارٍ للرتل فيما إذا كان فارغًا ومحاولة قراءة العنصر من الرتل، وفي تلك الحالة لن نجد شيئًا عند محاولة قراءة العنصر. في المقابل، يُعدّ التابع queue.poll()‎ بمثابة عمليةٍ ذرية atomic. يُمكِننا استخدام رتلٍ ينتمي إلى الصنف ConcurrentLinkedQueue مع مجمع خيوطٍ لحساب الصورة من المثال السابق، حيث سنُنشِئ جميع المهام المسؤولة عن حساب الصورة ونضيفها إلى الرتل، ثم سنُنشِئ الخيوط التي ستُنفِّذ تلك المهام، ونُشغِّلها. سيتضمَّن كل خيطٍ منها حلقة تكرار loop بحيث يُستدعى تابع الرتل poll()‎ بكل تكرارٍ لقراءة مهمةٍ منه ثم تُنفَّذ. نظراً لأن المهمة هي كائنٌ من النوع Runnable، فكل ما ينبغي أن يفعله الخيط هو استدعاء تابع المهمة run()‎؛ عندما يُعيد التابع poll()‎ القيمة null، فإن الرتل قد أصبح فارغًا أي أن جميع المهام قد أُسندَت لخيوطٍ أخرى، ويُمكِن عندها للخيط المُستدعِي الانتهاء. يُنفِّذ البرنامج MultiprocessingDemo2.java تلك الفكرة؛ حيث يَستخدِم رتلًا، اسمه taskQueue من النوع ConcurrentLinkedQueue<Runnable>‎ لحَمْل المهام. كما يَسمَح البرنامج للمُستخدِم بإلغاء العملية قبل انتهائها، حيث يَستخدِم متغيرًا منطقيًا متطايرًا volatile، اسمه running إشارةً للخيط بأن المُستخدِم قد ألغى العملية. عندما تُصبِح قيمة ذلك المتغير مساويةً للقيمة false، ينبغي أن ينتهي الخيط حتى لو لم يَكن الرتل فارغًا. تُعرِّف الشيفرة التالية الصنف المُتداخِل WorkerThread لتمثيل الخيوط: private class WorkerThread extends Thread { public void run() { try { while (running) { Runnable task = taskQueue.poll(); // اقرأ مهمةً من الرتل if (task == null) break; // لأن الرتل فارغ task.run(); // Execute the task; } } finally { threadFinished(); // ‫تذكّر أن الخيط قد انتهى. // ‫أضفناها بعبارة `finally` لنتأكَّد من استدعائها } } } يَستخدِم البرنامج الصنف المُتداخِل MandelbrotTask لتمثيل مهمة حساب صفٍ واحدٍ من البكسلات، حيث يُنفِّذ ذلك الصنف الواجهة Runnable، ويَحسِب تابعه run()‎ لون كل بكسلٍ بالصف، ثم يَنسَخ تلك الألوان إلى الصورة. تُوضِح الشيفرة التالية ما يفعله البرنامج عند بدء عملية المعالجة مع حذف قليلٍ من التفاصيل: taskQueue = new ConcurrentLinkedQueue<Runnable>(); // Create the queue. for (int row = 0; row < height; row++) { // عدد الصفوف الموجودة بالصورة MandelbrotTask task; task = ... ; // أنشِئ مهمةً لمعالجة صفٍ واحد من الصورة taskQueue.add(task); // أضف المهمة إلى الرتل } int threadCount = ... ; // عدد الخيوط الموجودة بالمجمع. يضبُطه المُستخدِم workers = new WorkerThread[threadCount]; running = true; // اضبط الإشارة قبل بدء تشغيل الخيوط threadsRemaining = workers; // عدد الخيوط قيد التشغيل for (int i = 0; i < threadCount; i++) { workers[i] = new WorkerThread(); try { workers[i].setPriority( Thread.currentThread().getPriority() - 1 ); } catch (Exception e) { } workers[i].start(); } تجدر الإشارة هنا إلى أنه من الضروري إضافة المهام إلى الرتل قبل بدء تشغيل الخيوط؛ لأننا نَستخدِم الرتل الفارغ بمثابة إشارةٍ إلى ضرورة انتهاء الخيوط؛ أي إذا وجدت الخيوط عند تشغيلها الرتل فارغًا، فستنتهي فورًا دون أن تُنجِز أي مهمة. جرِّب البرنامج "MultiprocessingDemo2"؛ فهو يَحسِب نفس الصورة التي يَحسبِها البرنامج "MultiprocessingDemo1"، ولكنه يختلف عنه بالترتيب الذي تُحسَب على أساسه الصفوف في حالة استخدام أكثر من خيط. إذا شاهدت البرنامج بحرص، فستلاحظ عدم إضافة صفوف البكسلات بالترتيب من أعلى إلى أسفل؛ لأن خيط الصف i+1 قد يُنهِي عمله قبل أن يُنهِي خيط الصف i عمله أو حتى ما يَسبقه من صفوف. ستلاحِظ هذا التأثير بقدرٍ أكبر إذا استخدمت عدد خيوطٍ أكبر مما يحتويه حاسوبك من معالجات. جرِّب 20 خيطًأ مثلًا. نمط المنتج والمستهلك والأرتال المعطلة يُنشِئ البرنامج "MultiprocessingDemo2" مجمع خيوطٍ جديد تمامًا بكل مرةٍ يَرسِم بها صورة، وهو ما يبدو سيئًا. أليس من المفترض إنشاء مجموعةٍ واحدةٍ فقط من الخيوط ببداية البرنامج، واستخدامها لحساب أي صورة؟ حيث أن الهدف من اِستخدَام مجمع الخيوط بالنهاية هو انتظار الخيوط للمهام الجديدة وتنفيذها. ولكننا، لم نُوضِّح حتى الآن أي طريقةٍ لجعل خيطٍ ينتظر قدوم مهمةٍ جديدة، حيث يُوفِّر الرتل المُعطِّل blocking queue ذلك. يُعدّ الرتل المُعطِّل تنفيذًا لإحدى أنماط المعالجة على التوازي، وهو نمط المُنتِج والمُستهلِك producer/consumer؛ حيث يُستخدَم هذا النمط في حالة وجود "مُنتِجٍ" واحدٍ أو أكثر لشيءٍ معين إلى جانب وجود "مُستهلِكٍ" واحد أو أكثر لذلك الشيء. لا بُدّ أن يَعمَل جميع المُنتِجين والمُستهِلِكين بنفس الوقت (أي معالجة على التوازي). إذا لم يَتوفَّر أي شيء للمعالجة، فسيضطّر المُستهلِك للانتظار إلى أن تتَوفَّر إحداها. قد يضطَّر المُنتِج ببعض التطبيقات للانتظار أحيانًا: إذا كان مُعدّل استهلاك الأشياء واحدًا لكل دقيقةٍ مثلًا، فمن غير المنطقي أن يكون معدّل إنتاجها اثنين لكل دقيقة مثلًا؛ لأنه سيؤدي إلى تراكمها بصورةٍ غير محدودة. ولذلك، لا بُدّ من تخصيص حدٍ أقصى لعدد الأشياء قيد الانتظار، وإذا وصلنا إلى ذلك الحد، لا بُدّ أن يتوقَّف المنتجون عن إنتاج أي أشياءٍ أخرى لبعض الوقت. والآن، سنحتاج إلى طريقةٍ لنقل الأشياء من المُنتجِين إلى المُستهلِكين، حيث يُعدّ الرتل الحل الأمثل لذلك: سيَضَع المنتجون الأشياء بأحد طرفي الرتل، وسيقرأها المُستهلِكون من الطرف الآخر. نظرًا لأننا نُجرِي معالجةً على التوازي، سنَستخدِم رتلًا متزامنًا، ولكننا نحتاج إلى ما هو أكثر من ذلك؛ فعندما يُصبِح الرتل فارغًا، نريد طريقةً تضطّر المستهلِكين للانتظار إلى أن يُوضَع شيءٌ جديدٌ بالرتل؛ وإذا أصبح ممتلئًا، نريد طريقةً تضطّر المُنتجِين للانتظار إلى أن يُتاح مكانٌ بالرتل. يُمثَّل كُلٌ من المُستهلِكين والمُنتجِين باستخدام الخيوط. إذا كان الخيط مُتوقِّفًا بانتظار حدوث شيءٍ معين، يُقَال أنه مُعطَّل blocked، ولذلك يُعدُّ الرتل المُعطِّل هو نوع الرتل الذي نحتاجه. عندما نَستخدِم رتلًا معطِّلًا، وكان ذلك الرتل فارغًا، ستؤدي عملية سحب dequeue عنصرٍ من الرتل إلى تعطيل المُستدعِي؛ أي إذا حاول خيطٌ معينٌ سحَب عنصرٍ من رتلٍ فارغ، فسيتوقَّف إلى أن يُتاح عنصرٌ جديد، وسيستيقظ عندها الخيط ويقرأ العنصر ويُكمِل عمله. بالمثل، إذا وصل الرتل إلى سعته القصوى، وحاول مُنتِجٌ معينٌ إدخال عنصرٍ به، فسيتعطَّل إلى أن يُتَاح مكانٌ بالرتل. تحتوي حزمة java.util.concurrent على صنفين يُنفِّذان الرتل المُعطِّل: LinkedBlockingQueue و ArrayBlockingQueue، وهما من الأنواع ذات المعاملات غير مُحدَّدة النوع parameterized types؛ أي يَسمحَا بتخصيص نوع العنصر الذي سيَحمله الرتل، ويُنفِّذ كلٌ منهما الواجهة BlockingQueue. إذا كان bqueue رتلًا مُعطِّلًا ينتمي إلى أحد الصنفين السابقين، فإنه يُعرِّف العمليات التالية: bqueue.take()‎: يَحذِف item من الرتل ويعيده؛ فإذا كان الرتل فارغًا عند استدعائه، يتعطَّل الخيط المُستدعِي إلى أن يُتاح عنصرٌ جديدٌ بالرتل. يُبلِّغ التابع عن استثناءٍ exception من النوع InterruptedException إذا قُوطَع الخيط أثناء تعطُّله. bqueue.put(item)‎: يُضيِف item إلى الرتل. إذا كان للرتل سعةً قصوى وقد أصبح ممتلئًا، يتعطَّل الخيط المُستدعِي إلى أن يُتَاح مكانٌ بالرتل. يُبلِّغ التابع عن استثناءٍ من النوع InterruptedException إذا قُوطَع الخيط أثناء تعطُّله. bqueue.add(item)‎: يُضيف item إلى الرتل في حالة وجود مكانٍ متاح. إذا كان للرتل سعةً قصوى وقد أصبح ممتلئًا، يُبلِّغ التابع عن استثناءٍ من النوع IllegalStateException، ولكنه لا يُعطِّل المُستدعِي. bqueue.clear()‎: يحذِف جميع العناصر من الرتل ويُهملها. تُعرِّف الأرتال المُعطِّلة بجافا الكثير من التوابع الأخرى. يتشابه التابع bqueue.poll(500)‎ مثلًا مع التابع bqueue.take()‎ باستثناء أنه يُعطِّل لمدة 500 ميللي ثانية بحدٍ أقصى، ولكن التوابع المذكورة بالأعلى كافيةٌ للأمثلة التالية. لاحِظ وجود تابعين لإضافة العناصر إلى الرتل؛ حيث يُعطِّل التابع bqueue.put(item)‎ المُستدعِي إذا لم يَكُن هناك أي مكانٍ آخر متاحٍ بالرتل، ولذلك يُستخدَم مع أرتال التعطيل محدودة السعة؛ بينما لا يُعطِّل التابع bqueue.add(item)‎ المُستدعِي، ولذلك يُستخدَم مع أرتال التعطيل التي تملُك سعةً غير محدودة. تُخصَّص السعة القصوى لرتلٍ من النوع ArrayBlockingQueue عند إنشائه. تُنشِئ الشيفرة التالية على سبيل المثال رتلًا مُعطِّلًا بإمكانه حَمْل ما يَصِل إلى 25 كائنٍ من النوع ItemType: ArrayBlockingQueue<ItemType> bqueue = new ArrayBlockingQueue<>(25); إذا اِستخدَمنا الرتل المُعرَّف بالأعلى، فسيُعطِّل التابع bqueue.put(item)‎ المُستدعِي إذا كان bqueue يحتوي على 25 عنصرٍ بالفعل؛ بينما يُبلِّغ التابع bqueue.add(item)‎ عن استثناءٍ في تلك الحالة. يضمَن ذلك عدم إنتاج العناصر بمعدّلٍ أسرع من مُعدّل استهلاكها. في المقابل، يُستخدَم الصنف LinkedBlockingQueue لإنشاء أرتالٍ مُعطِّلة بسعةٍ غير محدودة. ألقِ نظرةً على المثال التالي: LinkedBlockingQueue<ItemType> bqueue = new LinkedBlockingQueue<>(); تُنشِئ الشيفرة السابقة رتلًا بدون حدٍ أقصى لعدد العناصر التي يُمكِنه حملها. في تلك الحالة، لن يتسبَّب التابع bqueue.put(item)‎ بحدوث تعطيل نهائيًا، ولن يُبلِّغ التابع bqueue.add(item)‎ عن استثناءٍ من النوع IllegalStateException على الإطلاق. يُمكِننا اِستخدَام الصنف LinkedBlockingQueue إذا أردنا تجنُّب تعطيل المُنتجِين، ولكن من الجهة الأخرى، لا بُدّ أن نتأكَّد من بقاء الرتل بحجمٍ معقول. يؤدي التابع bqueue.take()‎ إلى حدوث تعطيلٍ إذا كان الرتل فارغًا لكلا الصنفين. يَستخدِم البرنامج التوضيحي MultiprocessingDemo3.java رتلًا من الصنف LinkedBlockingQueue بدلًا من الصنف ConcurrentLinkedQueue المُستخدَم في النسخة السابقة من البرنامج MultiprocessingDemo2.java. يحتوي الرتل في هذا المثال على عدة مهام (أي العناصر المنتمية للنوع Runnable)، ويُصرَّح عنه على أنه تابع نسخة instance variable اسمه taskQueue على النحو التالي: LinkedBlockingQueue<Runnable> taskQueue; عندما ينقر المُستخدِم على زر "Start" لحساب الصورة، نُضيف جميع مهام حساب الصورة إلى ذلك الرتل باستدعاء التابع taskQueue.add(task)‎ لكل مهمةٍ على حدى. من المهم أن يحدث ذلك دون تعطيل؛ لأننا نُنشِئ تلك المهام بخيط معالجة الأحداث events الذي لا ينبغي تعطيله. لن ينمو الرتل إلى ما لانهاية؛ لأن البرنامج يَعمَل على صورةٍ واحدةٍ فقط بكل مرة، وهناك مئاتٌ قليلةٌ من المهام للصورة الواحدة. على نحوٍ مشابه للنسخة السابقة من البرنامج، ستحذِف الخيوط العاملة المنتمية إلى مجمع الخيوط thread pool المهام من الرتل وتُنفِّذها، ولكنها -أي الخيوط- تُنشَئ مرةً واحدةً فقط ببداية البرنامج؛ أو بتعبيرٍ أدق عندما ينقر المُستخدِم على زر "Start" لأول مرة. يُعاد استخدام نفس تلك الخيوط لأي عددٍ من الصور، وفي حالة عدم وجود أي مهامٍ أخرى، سيُصبِح الرتل فارغًا، وستتعطَّل الخيوط إلى حين قدوم مهامٍ جديدة. تُنفِّذ تلك الخيوط حلقة تكرارٍ لا نهائية infinite loop، وتُعالِج المهام للأبد، ولكنها تقضِي وقتًا طويلًا معطَّلةً بانتظار إضافة مهمةٍ جديدةٍ إلى الرتل. انظر تعريف الصنف المُمثِل لتلك الخيوط: // 1 private class WorkerThread extends Thread { WorkerThread() { try { setPriority( Thread.currentThread().getPriority() - 1); } catch (Exception e) { } try { setDaemon(true); } catch (Exception e) { } start(); // يبدأ الخيط العمل بمجرد تشغيله } public void run() { while (true) { try { Runnable task = taskQueue.take(); // انتظر مهمة إذا كان ذلك ضروريًا task.run(); } catch (InterruptedException e) { } } } } [1] يُعرِّف هذا الصنف الخيوط العاملة الموجودة بمجمع الخيوط، حيث يَعمَل كائنٌ من هذا الصنف بحلقةٍ يَسترجِع كل تكرارٍ منها مهمةً من الرتل taskQueue ثم يستدعي التابع run()‎ الخاص بتلك المهمة. إذا كان الرتل فارغًا، يتعطَّل الخيط إلى أن تتوفَّر مهمةٌ جديدةٌ بالرتل. يتولى الباني مهمة بدء الخيط، وبالتالي لن يضطر البرنامج main لفعل ذلك. يَعمَل الخيط بأولويةٍ أقل من أولوية الخيط الذي اِستدعَى الباني. صُمِّم الصنف لكي يَعمَل بحلقةٍ لا نهائية تنتهي فقط عند إغلاق آلة جافا الافتراضية Java virtual machine، وهذا على فرض عدم تبليغ المهام المُنفَّذة عن أي استثناءاتٍ وهو افتراضٌ صحيحٌ بهذا البرنامج. يَضبُط الباني الخيط ليعمل مثل خيطٍ خفي، وبالتالي تنتهي آلة جافا الافتراضية تلقائيًا عندما تكون الخيوط الوحيدة الموجودة من النوع الخفي، أي لا يَمنَع وجود تلك الخيوط آلة جافا من الإغلاق. ينبغي فحص طريقة عمل مجمع الخيوط، حيث تُنشَأ الخيوط وتُشغَّل قبل وجود أي مهمة. يَستدعِي كل خيطٍ منها التابع taskQueue.take()‎ فورًا، ونظرًا لأن رتل المهام فارغ، تتعطَّل جميع الخيوط بمجرد تشغيلها. والآن، لكي نُعالِج صورةً معينة، يُنشِئ خيط معالجة الأحداث المهام الخاصة بتلك الصورة، ويُضيفها إلى الرتل. بمجرد حدوث ذلك، تعود الخيوط للعمل وتبدأ بمعالجة المهام، ويستمر الحال كذلك إلى أن يَفرُغ الرتل مرةً أخرى. في حالة تشغيل البرنامج بحاسوبٍ مُتعدّد المعالجات، تبدأ بعض الخيوط بمعالجة المهام المُضافة إلى الرتل بينما ما يزال خيط معالجة الأحداث مستمرٌ بإضافة المهام. عندما يُصبِح الرتل فارغًا، تتعطَّل الخيوط مجددًا إلى أن نرغب بمعالجة صورةٍ جديدة. إضافةً إلى ما سبق، قد نرغب بإلغاء معالجة صورةٍ معينةٍ قبل انتهائها، ولكننا لا نريد إنهاء الخيوط العاملة في تلك الحالة. عندما ينقر المُستخدِم على الزر "Abort"، يَستدعِي البرنامج التابع taskQueue.clear()‎، مما يَمنَع إسناد أي مهامٍ أخرى إلى الخيوط، ومع ذلك فمن المحتمل أن تكون بعض المهام قيد التنفيذ بالفعل بينما نُفرِّغ الرتل، وستكتمل بالتالي تلك المهام بعد إلغاء المعالجة المُفترَض كونهم أجزاءٌ منها، ولكننا لا نريد تطبيق خَرْج تلك المهام على الصورة. يُمكِننا حلّ تلك المشكلة بإسناد رقم وظيفة لكل وظيفة معالجة؛ حيث سيُخزَّن رقم الوظيفة الحالية بمتغير نسخة instance variable اسمه jobNum. ينبغي أن يحتوي كل كائن مُمثِّل لمهمة على تابع نسخة يُحدِّد الوظيفة التي تُعدّ تلك المهمة جزءًا منها. تزداد قيمة jobNum بمقدار الواحد عند انتهاء وظيفة؛ إما لأنها انتهت على نحوٍ طبيعي؛ أو لأن المُستخدِم قد ألغاها. عند اكتمال مهمةٍ معينة، لا بُدّ أن نوازن بين رقم الوظيفة المُخزَّن بكائن المهمة وبين jobNum؛ فإذا كانا متساويين، تكون المهمة جزءًا من الوظيفة الحالية، ويُطبَق خَرْجها على الصورة؛ أما إذا لم يكونا متساويين، تكون تلك المهمة جزءًا من الوظيفة السابقة، ويُهمَل خَرْجها. من المهم أن يكون الوصول إلى jobNum متزامنًا synchronized، وإلا قد يَفحَص خيطٌ معينٌ رقم الوظيفة بينما يزيده خيطٌ آخر، وعندها قد نَعرِض خرجًا معنيًّا لوظيفةٍ سابقة مع أننا ألغينا تلك الوظيفة. جميع التوابع التي تقرأ قيمة jobNum أو تُعدِّله في هذا البرنامج متزامنة. يُمكِنك قراءة شيفرة البرنامج لترى طريقة عملها. هناك ملاحظةٌ إضافية عن البرنامج "MultiprocessingDemo3"، وهي: نحن لم نُوفِّر أي طريقةٍ لإنهاء الخيوط العاملة ضمن ذلك البرنامج أي أنها ستستمر بالعمل إلى أن نُغلِق آلة جافا الافتراضية Java Virtual Machine. يُمكِننا السماح بإنهاء الخيوط قبل ذلك باستخدام متغيرٍ متطايرٍ volatile، اسمه running، وضبط قيمته إلى false عندما نرغب بإنهائها، وسنُعرِّف التابع run()‎ الموجود بالخيوط على النحو التالي: public void run() { while ( running ) { try { Runnable task = taskQueue.take(); task.run(); } catch (InterruptedException e) { } } } ومع ذلك، إذا كان هناك خيطٌ مُعطّلٌ نتيجةً لاستدعاء taskQueue.take()‎، فلن يتمكَّن من رؤية القيمة الجديدة للمتغيّر running قبل أن يعود للعمل. لنتأكَّد من إنهائه، يُمكِننا استدعاء التابع worker.interrupt()‎ لكل خيط worker بعد ضبط قيمة running إلى false. في حالة تنفيذ خيطٍ لمهمةٍ بينما نضبُط قيمة running إلى false، فإنه لن ينتهي حتى يُكمِل تلك المهمة. إذا كانت المهام قصيرةً نسبيًا، لن يُشكِّل ذلك مشكلة، ولكن إذا استغرقت المهام وقتًا أطول مما ترغب بانتظاره، فلا بُدّ أن تَفحَص المهام قيمة running دوريًا، وتنتهي إذا أصبحت قيمته مساويةً القيمة false. نهج ExecutorService لتنفيذ المهام يشيع استخدام مجمعات الخيوط thread pools بالبرمجة على التوازي، ولذلك، تُوفِّر جافا أدوات عالية المستوى لإنشاء مجمعات الخيوط وإدارتها. تُعرِّف الواجهة ExecutorService من حزمة java.util.concurrent خدماتٍ يُمكِنها تنفيذ المهام المُرسَلة إليها. يحتوي الصنف Executors على توابعٍ ساكنة static تُنشِئ أنواعًا مختلفةً من النوع ExecutorService. وبالأخص، يُنشِئ التابع Executors.newFixedThreadPool(n)‎ مجمع خيوطٍ مُكوَّن من عدد n من الخيوط، حيث n هي عدد صحيح. تُنشِئ الشيفرة التالية مجمع خيوط مُكوّنٍ من خيطٍ واحدٍ لكل معالج: int processors = Runtime.getRuntime().availableProcessors(); ExecutorService executor = Executors.newFixedThreadPool(processors); يُستخدَم التابع executor.execute(task)‎ لإرسال كائنٍ من النوع Runnable لتنفيذه، ويعود على الفور بعد وضعه للمهمة داخل رتل المهام المُنتظِرَة. تَحذِف الخيوط الموجودة بمجمع الخيوط المهام من الرتل وتُنفِّذها. يُخبِر التابع executor.shutdown()‎ مجمع الخيوط بأن عليه الانتهاء بعد تنفيذ جميع المهام المُنتظِرَة، ويعود التابع على الفور دون أن ينتظر انتهاء الخيوط. بعد استدعاء ذلك التابع، لا يُسمَح بإضافة مهامٍ جديدة. يُمكِنك استدعاء shutdown()‎ أكثر من مرة، ولن يُعدّ ذلك خطأً. لا تُعدّ الخيوط الموجودة بمجمع الخيوط خيوطًا خفية daemon threads؛ أي في حالة انتهاء الخيوط الأخرى دون إغلاق الخدمة، يكون وجود تلك الخيوط كافٍ لمنع إغلاق آلة جافا الافتراضية. يتشابه التابع executor.shutdownNow()‎ مع التابع executor.shutdown()‎، إلا أنه يُهمِل المهام التي ما تزال قيد الانتظار بالرتل، وتُكمِل الخيوط المهام التي كانت قد حُذفت من الرتل بالفعل قبل الإغلاق. يَختلف البرنامج التوضيحي MultiprocessingDemo4.java عن البرنامج MultiprocessingDemo3؛ حيث يَستخدِم النوع ExecutorService بدلًا من الاستخدام المباشر للخيوط والأرتال المُعطِّلة. نظرًا لعدم وجود طريقةٍ بسيطةٍ تَسمَح للنوع ExecutorService بتجاهُل المهام المُنتظِرَة دون أن يُغلَق، يُنشِئ البرنامج "MultiprocessingDemo4" كائنًا جديدًا من النوع ExecutorService لكل صورة. يُمكِننا تمثيل المهام المُستخدَمة مع النوع ExecutorService بكائناتٍ من النوع Callable<T>‎، حيث يُمثِّل ذلك النوع واجهة نوع دالة functional interface ذات معاملاتٍ غير مُحدَّدة النوع، ويُعرِّف التابع call()‎، الذي لا يستقبل أي معاملاتٍ ويعيد النوع T. يُمثِل النوع Callable مهمةً تُخرِج قيمة. يُمكِننا إرسال كائنٍ c من النوع Callable إلى النوع ExecutorService باستدعاء التابع executor.submit(c)‎، حيث تُنفَّذ المهمة من النوع Callable بلحظةٍ ما في المستقبل. في تلك الحالة، كيف سنَحصُل على نتيجة المعالجة عند اكتمالها؟ يُمكِننا حل تلك المشكلة باستخدام واجهةٍ أخرى هي Future<T>‎، التي تُمثِّل قيمةً من النوع T قد تكون غير متاحةٍ حتى وقتٍ ما بالمستقبل. يعيد التابع executor.submit(c)‎ قيمةً من النوع Future تُمثِّل نتيجة المعالجة المؤجَّلة. يُعرِّف كائنٌ v من النوع Future مجموعةً من التوابع، مثل الدالة المنطقية v.isDone()‎ التي يُمكِننا استدعاؤها لفحص فيما إذا كانت نتيجة المعالجة قد أصبحت متاحة؛ وكذلك التابع v.get()‎ الذي يسترجع نتيجة المعالجة المؤجَّلة، وسيُعطًّل إلى أن تُصبِح القيمة متاحة، كما قد يُبلِّغ عن استثناءات، ولذلك ينبغي استدعاؤه ضمن تعليمة try..catch. يَستخدِم المثال ThreadTest4.java الأنواع Callable و Future و ExecutorService لعدّ عدد الأعداد الأولية الواقعة ضمن نطاقٍ معينٍ من الأعداد الصحيحة؛ كما يُجرِي نفس المعالجة التي أجراها البرنامج ThreadTest2.java في قسم الإقصاء التشاركي Mutual Exclusion وتعليمة التزامن synchronized من مقال مقدمة إلى الخيوط Threads في جافا. ستَعُدّ كل مهمةٍ فرعية في هذا البرنامج عدد الأعداد الأولية ضمن نطاقٍ أصغر من الأعداد الصحيحة، وستُمثَّل تلك المهام الفرعية من خلال كائناتٍ من النوع Callable<Integer>‎ المُعرَّفة بالصنف المتداخل nested التالي: // 1 private static class CountPrimesTask implements Callable<Integer> { int min, max; public CountPrimesTask(int min, int max) { this.min = min; this.max = max; } public Integer call() { int count = countPrimes(min,max); // يبدأ بالعدّ return count; } } [1] تعدّ الكائنات المنتمية إلى هذا الصنف الأعداد الأولية الموجودة ضمن نطاقٍ معين من الأعداد الصحيحة من min إلى max. تُمرَّر قيمة المتغيرين min و max مثل معاملاتٍ للباني. يحسب التابع call()‎ عدد الأعداد الأولية ثم يعيدها. ستُرسَل جميع المهام الفرعية إلى مجمع خيوط مُنفَّذ باستخدام النوع ExecutorService، وتُخزَّن النتائج من النوع Future التي يعيدها داخل مصفوفةٍ من النوع ArrayList. ألقِ نظرةً على الشيفرة التالية: int processors = Runtime.getRuntime().availableProcessors(); ExecutorService executor = Executors.newFixedThreadPool(processors); ArrayList<Future<Integer>> results = new ArrayList<>(); for (int i = 0; i < numberOfTasks; i++) { CountPrimesTask oneTask = . . . ; Future<Integer> oneResult = executor.submit( oneTask ); results.add(oneResult); // خزِّن الكائن الذي يُمثِّل النتيجة المؤجَلة } لا بُدّ أن نُضيف الأعداد الصحيحة الناتجة عن المهام الفرعية إلى المجموع النهائي. سنحصل أولًا على خَرْج تلك المهام باستدعاء التابع get()‎ لعناصر المصفوفة المنتمية إلى النوع Future. لن تكتمل العملية إلا بعد انتهاء جميع المهام الفرعية، لأن التابع يُعطِّل المُستدعِي إلى أن تتوفَّر النتيجة. ألقِ نظرةً على الشيفرة التالية: int total = 0; for ( Future<Integer> res : results) { try { total += res.get(); // انتظر اكتمال المهمة } catch (Exception e) { // لا ينبغي أن تحدث بهذا البرنامج } } تابعا الانتظار Wait والتنبيه Notify إذا كنا نريد كتابة تنفيذٍ للرتل المعطِّل، فينبغي أن نُعطِّل الخيط إلى حين وقوع حدثٍ معين؛ أي أن ينتظر الخيط وقوع ذلك الحدث، وينبغي أن نُبلِّغه عند وقوعه بطريقةٍ ما. سنَستخدِم لذلك خيطين؛ حيث يقع الفعل المُسبِّب للحدث المنتظَر (مثل إضافة عنصرٍ إلى رتل) بخيطٍ غير الخيط المُعطَّل. لا يُمثِّل ما يَلي مشكلةً للأرتال المُعطِّلة فقط؛ ففي حالة وجود خيطٍ يُنتِج خرجًا يحتاج إليه خيطٌ آخر، فإن ذلك يَفرِض نوعًا من التقييد على الترتيب الذي ينبغي للخيوط أن تُنفِّذ العمليات على أساسه. إذا وصلنا إلى النقطة التي يحتاج خلالها الخيط الثاني إلى الخرج الناتج عن الخيط الأول، قد يضطّر الخيط الثاني إلى التوقُّف وانتظار إتاحة ذلك الخرج؛ ونظرًا لأنه لا يستطيع الاستمرار، فإنه قد ينام sleep، ولا بُدّ في تلك الحالة من توفير طريقةٍ لتنبيهه عندما يُصبِح الخرج متاحًا، حتى يستيقظ ويُكمِل عملية المعالجة. تُوفِّر جافا بالطبع طريقةً لتنفيذ هذا النوع من الانتظار والتنبيه؛ حيث يحتوي الصنف Object على تابعي النسخة wait()‎ و notify()‎، ويُمكِن استخدامهما مع أي كائن، كما يمكن للأرتال المعطِّلة اِستخدَام تلك التوابع ضمن تنفيذها الداخلي، ولكنها منخفضة المستوى وعرضةً للأخطاء؛ ولذلك يُفضَّل اِستخدام أساليب التحكُّم عالية المستوى مثل أرتال الأولوية قدر الإمكان. مع ذلك، من الجيد معرفة القليل عن التابعين wait()‎ و notify()‎، فلربما قد تحتاج إلى استخدامهما مباشرةً. من غير المعروف فيما إذا كانت أصناف جافا القياسية للأرتال المُعطِّلة تَستخدِم هذين التابعين فعليًا، خاصةً مع توقُّر طرائقٍ أخرى لحل مشكلة الانتظار والتنبيه. السبب وراء ضرورة ربط التابعين wait()‎ و notify()‎ بالكائنات واضح، وبالتالي ليس هناك داعٍ للقلق بشأن ذلك، فهو يَسمَح على الأقل بتوجيه تنبيهاتٍ من أنواعٍ مختلفة إلى مستقبلين من أنواعٍ مختلفة اعتمادًا على تابع الكائنnotify()‎ المُستدعى. عندما يَستدعِي خيطٌ ما التابع wait()‎ الخاص بكائنٍ معين، يتوقَّف ذلك الخيط وينام إلى حين استدعاء التابع notify()‎ الخاص بنفس الكائن، حيث سيكون استدعاؤه ضروريًا من خلال خيطٍ آخر؛ لأن الخيط الذي اِستدعَى wait()‎ سيكون نائمًا. تَعمَل إحدى الأنماط الشائعة على النحو التالي: يستدعِي خيط A التابع wait()‎ عندما يحتاج إلى الخرج الناتج من خيط B، ولكن ذلك الخرج غير متاحٍ بعد. عندما يُحصِّل الخيط B الخرج المطلوب، فإنه يَستدعِي التابع notify()‎ الذي سيوقِظ الخيط A إذا كان منتظرًا ليتمكَّن من اِستخدَام الناتج. في الواقع، ليس من الخطأ استدعاء التابع notify()‎ حتى لو لم يَكُن هناك أي خيوطٍ مُنتظِرَة، فليس لها أي تأثير. لنُنفِّذ ذلك، ينبغي أن يُنفِّذ الخيط A شيفرةً مشابهةً لما يلي، حيث obj هو كائن: if ( resultIsAvailable() == false ) obj.wait(); // انتظر تنبيهًا بأن النتيجة مُتاحة useTheResult(); بينما ينبغي أن يُنفِّذ الخيط B شيفرةً مشابهةً لما يَلي: generateTheResult(); obj.notify(); // أرسل تنبيهًا بأن النتيجة قد أصبحت متاحة تعاني تلك الشيفرة من حالة تسابق race condition، فقد يُنفِّذ الخيطان شيفرتهما بالترتيب التالي: // يفحص الخيط‫ A التابع `resultIsAvailable()‎` ولا يجد النتيجة بعد، لذلك، يُنفِّذ تعليمة `obj.wait()‎`، ولكن قبل أن يفعل، 1. Thread A checks resultIsAvailable() and finds that the result is not ready, so it decides to execute the obj.wait() statement, but before it does, // ‫ينتهي الخيط B من عمله ويَستدعِي التابع `obj.notify()‎` 2. Thread B finishes generating the result and calls obj.notify() // ‫يَستدعِي الخيط A التابع `obj.wait()‎` لينتظر تنبيهًا بتوفُّر النتيجة 3. Thread A calls obj.wait() to wait for notification that the result is ready. ينتظر الخيط A بالخطوة الثالثة تنبيهًا لن يحدث أبدًا؛ لأن notify()‎ قد اُستدعيَت بالفعل بالخطوة الثانية. يُمثِّل ذلك نوعًا من القفل الميت deadlock الذي يُمكِنه أن يترك الخيط A مُنتظِرًا للأبد. نحتاج إذًا إلى نوعٍ من المزامنة synchronization. يكمن حل تلك المشكلة في وضع شيفرة الخيطين A و B داخل تعليمة synchronized، ومن البديهي أن تكون المزامنة بناءً على نفس الكائن obj المُستخدَم عند استدعاء wait()‎ و notify()‎. نظرًا لأهمية استخدام المزامنة عند كل استدعاءٍ للتابعين wait()‎ و notify()‎ تقريبًا، جعلته جافا أمرًا ضروريًا؛ أي بإمكان خيطٍ معينٍ استدعاء obj.wait()‎ أو obj.notify()‎ فقط إذا كان ذلك الخيط قد حَصَل على قفل المزامنة المُرتبِط بالكائن obj؛ أما إذا لم يَكُن قد حَصَل عليه، يحدث استثناء من النوع IllegalMonitorStateException. لا يتطلَّب هذا الاستثناء معالجةً إجباريةً ولا يُلتقَط على الأرجح. علاوةً على ذلك، قد يُبلِّغ التابع wait()‎ عن اتستثناءٍ من النوع InterruptedException، ولذلك لا بُدّ من استدعائه ضمن تعليمة try لمعالجته. لنفحص الآن طريقة وصول خيطٍ معينٍ إلى نتيجةٍ يحسبها خيطٌ آخر. يُعدّ ذلك مثالًا مبسطًا على مشكلة المُنتِج والمُستهلِك producer/consumer، حيث يُنتَج عنصرٌ واحدٌ فقط ثم يُستهلَك. لنفترض أن لدينا متغيرًا تشاركيًا، اسمه sharedResult مُستخدَمٌ لنقل النتيجة من المُنتِج إلى المُستهلِك. عندما تُصبِح النتيجة جاهزة، يضبُط المُنتِج ذلك المتغير إلى قيمةٍ غير فارغة. يُحدِّد المُستهلِك من الجهة الأخرى فيما إذا كانت النتيجة جاهزةً أم لا بفحص قيمة المتغير sharedResult إذا كانت فارغة. سنَستخدِم مُتغيّرًا اسمه lock للمزامنة. يُمكِننا كتابة شيفرة الخيط المُمثِّل للمُنتِج على النحو التالي: makeResult = generateTheResult(); // غير متزامن synchronized(lock) { sharedResult = makeResult; lock.notify(); } بينما سيُنفِّذ المُستهلِك الشيفرة التالية: synchronized(lock) { while ( sharedResult == null ) { try { lock.wait(); } catch (InterruptedException e) { } } useResult = sharedResult; } useTheResult(useResult); // Not synchronized! لاحِظ أن استدعاء كُلٍ من التابعين generateTheResult()‎ و useTheResult()‎ غير متزامن، لنَسمَح بتنفيذهما على التوازي مع الخيوط الأخرى التي قد تُجرِي تزامنًا بناءً على lock، ولكن نظرًا لأن المتغير sharedResult تشاركي، كان من الضروري أن تكون جميع مراجِعه references متزامنة؛ أي لا بُدّ من كتابتها داخل تعليمة synchronized، مع محاولة تنفيذ أقل ما يُمكِن عمومًا داخل كتل الشيفرة المتزامنة. ربما لاحظت شيئًا مضحكًا بالشيفرة: لا ينتهي lock.wait()‎ قبل تنفيذ lock.notify()‎، ولكن نظرًا لأن كليهما مكتوبٌ داخل تعليمة synchronized بتزامنٍ مبني على الكائن نفسه، قد تتساءل: أليس من المستحيل تنفيذ هذين التابعين بنفس الوقت؟ في الواقع، يُعدّ التابع lock.wait()‎ حالةً خاصة؛ فعندما يَستدعِي خيطٌ ما التابع lock.wait()‎، فإنه يترك قفله بالضرورة على كائن المزامنة، مما يَسمَح لخيطٍ آخرٍ بتنفيذ كتلة شيفرة داخل تعليمة synchronized(lock)‎ أخرى يوجد بداخلها استدعاءٌ للتابع lock.notify()‎. وبالتالي، بعدما يُنهِي الخيط الثاني تنفيذ تلك الكتلة، يعود القفل إلى الخيط الأول المُستهلِك مما يُمكِّنه من إكمال عمله. تُنتَج في نمط المُنتِج والمُستهلِك العادي عدة نتائجٍ بواسطة خيط مُنتِجٍ واحدٍ أو أكثر، وتُستهلَك بواسطة خيط مُستهلِكٍ واحدٍ أو أكثر، وبدلًا من وجود كائن sharedResult وحيد، تجد قائمةً بالكائنات المُنتَجَة التي لم تُستهلَك بعد. لنفحص طريقة فعل ذلك باستخدام صنفٍ بسيطٍ للغاية يُنفِّذ العمليات الثلاثة على رتلٍ من النوع LinkedBlockingQueue<Runnable>‎، الذي اِستخدَمناه بالبرنامج MultiprocessingDemo3. ألقِ نظرةً على الشيفرة التالية: import java.util.LinkedList; public class MyLinkedBlockingQueue { private LinkedList<Runnable> taskList = new LinkedList<Runnable>(); public void clear() { synchronized(taskList) { taskList.clear(); } } public void add(Runnable task) { synchronized(taskList) { taskList.addLast(task); taskList.notify(); } } public Runnable take() throws InterruptedException { synchronized(taskList) { while (taskList.isEmpty()) taskList.wait(); return taskList.removeFirst(); } } } سنَستخدِم كائنًا من ذلك الصنف بديلًا عن الكائن taskQueue بالبرنامج "MultiprocessingDemo3". فضَّلنا إجراء المزامنة بناءً على الكائن taskList، ولكن كان من الممكن إجراؤها بناءً على أي كائنٍ آخر. يُمكِننا في الحقيقة استخدام توابعٍ متزامنة synchronized methods، وهو ما سيُكافِئ المزامنة بناءً على this. من الضروري أن يكون استدعاء التابع taskList.clear()‎ مبنيًا على نفس الكائن حتى لو لم نَستدعِي wait()‎ أو notify()‎؛ وإذا لم نَفعَل ذلك، قد تحدث حالة تسابق race condition، ألا وهي: قد تُفرَّغ القائمة بعدما يتأكَّد التابع take()‎ من أن القائمة taskList غير فارغة وقبل أن يحاول حذف عنصرٍ منها. في تلك الحالة، ستُصبِح القائمة فارغة عند لحظة استدعاء taskList.removeFirst()‎ مما سيتسبَّب بحدوث خطأ. في حالة تواجد عدة خيوطٍ متزامنة بناءً على كائن obj ومُنتظِرةٍ للتنبيه. يُوقِظ التابع obj.notify()‎ عند استدعائه واحدًا فقط من تلك الخيوط المُنتظِرة؛ وإذا أردت أن توقظها جميعًا، ينبغي أن تَستدعِي التابع obj.notifyAll()‎. من المناسب اِستخدَام التابع obj.notify()‎ بالمثال السابق؛ لأن الخيوط المُستهلِكة فقط هي الخيوط المُعطَّلة، ونحن نريد إيقاظ مُستهلِكٍ واحدٍ فقط عند إضافة مهمةٍ إلى الرتل، ولا يُهِم أي مُستهلِكٍ تُسنَد إليه المهمة. في المقابل، إذا كان لدينا رتلٌ مُعطِّل blocking queue بسعةٍ قصوى، أي أنه قد يُعطِّل المُنتجِين أو المُستهلِكِين؛ فعند إضافة مهمةٍ إلى الرتل، ينبغي التأكُّد من تنبيه خيط مُستهلِكٍ لا خيط مُنتِج، ويُمثِّل استدعاء التابع notifyAll()‎ بدلًا من التابع notify()‎ إحدى حلول تلك المشكلة، لأنه سيُنبِّه جميع الخيوط بما في ذلك أي خيط مُستهلِكٍ مُنتظِر. قد يعطيك اسم التابع obj.notify()‎ انطباعًا خاطئًا. لا يُنبِّه ذلك التابع الكائن obj بأي شيء، وإنما يُنبِّه الخيط الذي اِستدعَى التابع obj.wait()‎ إذا كان موجودًا. بالمثل، لا ينتظر الكائن obj بالاستدعاء obj.wait()‎ أي شيء، وإنما الخيط المُستدعِي هو من ينتظر. وفي ملاحظة أخيرة بخصوص wait: هناك نسخةٌ أخرى من التابع wait()‎، وهي تَستقبِل زمنًا بوحدة الميللي ثانية مثل مُعامِل؛ وهنا سينتظر الخيط المُستدعِي للتابع obj.wait(milliseconds)‎ تنبيهًا لفترةٍ تَصِل إلى القيمة الُممرَّرة بحدٍ أقصى؛ وإذا لم يحدث التنبيه خلال تلك الفترة، يستيقظ الخيط ويُكمِل عمله دون تنبيه. تُستخدَم تلك الخاصية عمليًا لتَسمَح لخيطٍ مُنتظِر بالاستيقاظ كل فترة لإنجاز مهمةٍ دوريةٍ معينة، مثل التسبُّب في ظهور رسالة مثل "Waiting for computation to finish". لنفحص الآن مثالًا يَستخدِم التابعين wait()‎ و notify()‎ ليَسمَح لخيطٍ بالتحكُّم بخيطٍ آخر. يَحِلّ البرنامج التوضيحي TowersOfHanoiGUI.java مسألة أبراج هانوي التي تعرَّضنا لها بالقسم مشكلة أبراج هانوي Hanoi من مقال التعاود recursion في جافا، ويُوفِّر أزرارًا تَسمَح للمُستخدِم بالتحكُّم بتنفيذ الخوارزمية. يَستطيع المُستخدِم مثلًا النقر على زر "Next Step" ليُنفِّذ خطوةً واحدةً من الحل، والتي تُحرِّك قرصًا واحدًا من كومةٍ لأخرى. عند النقر على زر "Run"، تُنفَّذ الخوارزمية أتوماتيكيًا دون تدخُّل المُستخدِم، ويتبدَّل النص المكتوب على الزر من "Run" إلى "Pause". عند النقر على "Pause"، يتوقَّف التشغيل التلقائي. يُوفِّر البرنامج الزر "Start Over"، الذي يُلغي الحل الحالي، ويعيد المسألة إلى حالتها الابتدائية. تَعرِض الصورة التالية شكل البرنامج بإحدى خطوات الحل، ويُمكِنك رؤية الأزرار المذكورة: يوجد خيطان بهذا البرنامج؛ حيث يُنفِّذ الأول خوارزميةً تعاودية recursive لحلّ المسألة؛ ويُعالِج الآخر الأحداث الناتجة عن أفعال المُستخدِم. عندما ينقر المُستخدِم على أحد الأزرار، تُستدعَى إحدى التوابع بخيط معالجة الأحداث، ولكن من يَستجِيب فعليًا للحدث هو الخيط المُنفِّذ للتعاود؛ فقد يُنفِّذ مثلًا خطوةً واحدةً من الحل أو يبدأه من جديد. لا بُدّ أن يُرسِل خيط معالجة الأحداث نوعًا من الإشارة إلى خيط الحل من خلال ضبط قيمة متغيرٍ يتشاركه الخيطان. اسم هذا المتغير بالبرنامج هو status، وقيمه المُحتمَلة هي الثوابت GO و PAUSE و STEP و RESTART. عندما يُعدِّل خيط معالجة الأحداث قيمة ذلك المتغيّر، لا بُدّ أن يلاحظ خيط الحل القيمة الجديدة للمتغير، ويستجيب على أساسها؛ فإذا كانت قيمة status هي PAUSE، لا بُدّ أن يتوقَّف الخيط بانتظار نَقْر المُستخدِم على زر "Run" أو "Next Step"، ويُمثِّل ذلك الحالة المبدئية عند بدء البرنامج؛ أما إذا نقر المُستخدِم على زر "Next Step"، يَضبُط خيط معالجة الأحداث قيمة status إلى "STEP"، وبالتتابع، لا بُدّ أن يلاحِظ خيط الحل القيمة الجديدة، ويستجيب بتنفيذ خطوةٍ واحدةٍ من الحل، ثم يعيد ضَبْط قيمة status إلى PAUSE مرةً أخرى. إذا نقر المُستخدِم على زر "Run"، تُضبَط قيمة status إلى "GO"، وينبغي أن يُنفِّذ خيط الحل الخوارزمية أتوماتيكيًا؛ وإذا نقر المُستخدِم على زر "Pause" بينما الحل مُشغَّل، تُضبَط قيمة status إلى "PAUSE"، وينبغي أن يعود خيط الحل إلى حالة الإيقاف؛ أما إذا نقر المُستخدِم على زر "Start Over"، يَضبُط خيط معالجة الأحداث قيمة المُتغيّر status إلى "RESTART"، ولا بُدّ أن يُنهِي خيط الحل حلّه الحالي. ما يُهمّنا بهذا المثال هو الحالة التي يتوقَّف خلالها خيط الحل؛ حيث يكون الخيط نائمًا في تلك الحالة، ولا يكون بإمكانه رؤية القيمة الجديدة للمتغير status إلا إذا أيقظناه. سنَستخدِم التابع wait()‎ بخيط الحل لجعله ينام، وسنَستخدِم التابع notify()‎ بخيط معالجة الأحداث عندما نُعدِّل قيمة المتغير status لكي نُوقِظ خيط الحل. تعرض الشيفرة التالية التوابع التي تَستجيب لحدث النقر على الأزرار. عندما ينقر المُستخدِم على زر معين، يُعدِّل التابع المقابل لذلك الزر قيمة المتغير status، ثم يَستدعِي التابع notify()‎ لكي يُوقِظ خيط الحل: synchronized private void doStopGo() { if (status == GO) { // التحريكة مُشغَّلة. أوقفها status = PAUSE; nextStepButton.setDisable(false); runPauseButton.setText("Run"); } else { // Animation is paused. Start it running. status = GO; nextStepButton.setDisable(true); // يُعطَّل عند تشغيل التحريكة runPauseButton.setText("Pause"); } notify(); // أيقظ الخيط ليتمكَّن من رؤية الحالة الجديدة } synchronized private void doNextStep() { status = STEP; notify(); } synchronized private void doRestart() { status = RESTART; notify(); } لاحِظ أن تلك التوابع متزامنة لتَسمَح باستدعاء notify()‎. تذكَّر أنه لا بُدّ للخيط المُستدعِي للتابع notify()‎ ضمن كائنٍ معين أن يكون قد حَصَل على قفل المزامنة المُرتبِط بذلك الكائن. في هذه الحالة، يكون كائن المزامنة هو this. تُعدّ المزامنة ضروريةً لأنه من الممكن أن تحدث حالات التسابق نظرًا لإمكانية خيط الحل أن يُعدِّل قيمة المتغير status. يَستدعِي خيط الحل تابعًا اسمه checkStatus()‎ ليَفحَص قيمة status؛ فإذا كانت قيمة status تُساوِي "PAUSE"، يَستدعِي ذلك التابع بدوره التابع wait()‎ مما يؤدي إلى توقُّف خيط الحل إلى حين استدعاء خيط معالجة الأحداث للتابع notify()‎. لاحِظ أن التابع checkStatus()‎ يُبلِّغ عن استثناءٍ من النوع IllegalStateException إذا كانت قيمة status تُساوِي "RESTART": synchronized private void checkStatus() { while (status == PAUSE) { try { wait(); } catch (InterruptedException e) { } } // بالوصول إلى تلك النقطة، تكون الحالة‫ RUN أو STEP أو RESTART if (status == RESTART) throw new IllegalStateException("Restart"); // بالوصول إلى تلك النقطة، تكون الحالة‫ RUN أو STEP وينبغي أن يستمر الحل } يَضبُط التابع run()‎ الخاص بخيط الحل الحالة المبدئية للمسألة، ثم يَستدعِي التابع solve()‎ لحلها، كما يُنفِّذ حلقةً لا نهائية ليتمكَّن من حل المسألة عدة مرات. يَستدعِي التابع run()‎ التابع checkStatus()‎ قبل أن يبدأ الحل، ويَستدعِي التابع solve()‎ التابع checkStatus()‎ بعد كل حركة. إذا بلَّغ التابع checkStatus()‎ عن استثناءِ من النوع IllegalStateException، يُنهَى استدعاء solve()‎ مبكرًا. كنا قد استخدمنا نفس طريقة التبليغ عن استثناء لإنهاء خوارزميةٍ تعاوديةٍ من قبل بالقسم التعاود داخل الخيوط من المقال السابق. يُمكِنك الإطلاع على الشيفرة الكاملة للبرنامج TowersOfHanoiGUI.java لترى الطريقة التي دمجنا بها جميع تلك الأجزاء إلى البرنامج النهائي، وسيُعينك فهمه على تعلُم طريقة استخدام wait()‎ و notify()‎ مباشرةً. ترجمة -بتصرّف- للقسم Section 3: Threads and Parallel Processing من فصل Chapter 12: Threads and Multiprocessing من كتاب Introduction to Programming Using Java. اقرأ أيضًا المقال السابق: البرمجة باستخدام الخيوط threads في جافا مقدمة إلى الخيوط Threads في جافا كيفية إنشاء عدة خيوط وفهم التزامن في جافا
    1 نقطة
  31. تعد مكتبة jQuery واحدة من أكثر المكتبات استخداما للإضافة على الصفحات؛ إذ أنها تجعل من التعامل مع نموذج كائن المستند Document object model, DOM أمرا في منتهى اليسر. لا شك أن السهولة في التعامل سبب أساسي في شعبية jQuery، إذ يبدو بالإمكان فعل أي شيء نريده عن طريق هذه المكتبة. توجد، من بين الخيارات المتاحة أمامنا، مقاطع Snippets تجنح إلى الظهور المرة تلو الأخرى. سنعرِض في هذا المقال إلى عشرة مقاطِع سيستخدمها الجميع، من المبتدئ إلى المتمكن، مرارا وتكرارا. زر العودة إلى الأعلى// عد إلى الأعلى $('a.top').click(function(){ $(document.body).animate({scrollTop : 0}, 800); return false; }); // Anchor tag أنشئ وسما للمربط <a class="top" href="#">عد إلى الأعلى</a>يتضح أننا لا نحتاج إلى إضافة Plugin لـ jQuery من أجل الحصول على تحريك سهل إلى الأعلى؛ يكفي استخدام دالتي animate و scrollTop. يمكن تغيير المكان الذي يحُط فيه شريط التمرير عبر تغيير قيمة scrollTop. في المثال أعلاه استخدمنا القيمة 0 لأننا نريد العودة إلى أعلى الصفحة، لكن لو أردنا زَيحانًا offset بقيمة 100px فيمكن إدراج هذه القيمة. يتلخص ما فعلناه في تحريك جسم المستند Document body طوال 800 ملي ثانيّة حتى يصل إلى القمّة. التحقق من تحميل الصور$('img').load(function() { console.log('image load successful'); });تحتاج أحيانا إلى التأكد من تحميل كل الصور قبل إكمال السكربت؛ تؤدي الأسطر الثلاثة أعلاه هذه المهمة بكل يُسر. يمكن أيضا التحقق من تحميل صورة معينة عبر إبدال وسم tag بمعرِّف ID أو صنف Class. تصحيح الصور المعطوبة تلقائيا$('img').error(function(){ $(this).attr('src', 'img/broken.png'); });تحصل أحيانا أعطاب في روابط الصور على الموقع يكون معها إبدال الروابط يدويا أمرا صعبا. يعمل المقطع أعلاه على إبدال الصور المعطوبة تلقائيا مما ينقِذ من الكثير من المشاكل. تبديل الصنف عند الحومان Hover$('.btn').hover(function(){ $(this).addClass('hover'); }, function(){ $(this).removeClass('hover'); } );نرغب عادة في تغيير مظهر العناصر القابلة للنقر في صفحة الويب عندما يحوم حولها المؤشر وهو بالضبط ما تفعله الأسطر في المقطع أعلاه؛ فتضيف صنفا للعنصر عند الحوم حوله ثم تزيل الصنف عندما يكُفّ المستخدِم. كل ما عليك فعله هو إضافة التنسيق المرغوب ضمن ملف CSS. تعطيل حقول الإدخال$('input[type="submit"]').attr("disabled", true);قد تريد تعطيل زر الإرسال أو حقل إدخال إلى أن يؤدي المستخدم إجراء معينا (التأشير على صندوق “قرأتُ الشروط”، مثلا). يضيف المقطع خاصية disabled (مُعطَّل) إلى الحقل مما يتيح لك تفعيله عندما تريد. كل ما عليك فعله لتفعيل الحقل هو تنفيذ الدالة removeAttr مع تمرير المُعطى disabled على النحو التالي: $('input[type="submit"]').removeAttr("disabled");إيقاف تحميل الروابط$('a.no-link').click(function(e){ e.preventDefault(); });قد نود أن تؤدي الروابط أعمالا أخرى غير الانتقال إلى صفحة أو حتى إعادة تحميلها، وهو ما تعمل عليه الأسطر أعلاه عبر تعطيل الإجراء الافتراضي Default action. قد يكون السبب - على سبيل المثال - تنشيطَ سكربت آخر. التبديل بين تأثيريْ التلاشي Fade والانزلاق Slide// تلاش $(".btn").click(function() { $(".element").fadeToggle("slow"); }); // تبديل $(".btn").click(function() { $(".element").slideToggle("slow"); });تأثيرا التّلاشي والانزلاق من أكثر التّأثيرات في jQuery استخداما. عندما نريد فقط عرض عنصر عند النقر فإن دالتي fadeIn وslideDown ملائمتان تماما. لكن إن أردنا أن يظهر العنصر بعد النقرة الأولى ثم يختفي بعد الثانية فهذا المقطع يؤدي المهمة بنجاح. تأثير الطّيّ// أغلق كل اللوحات $('#accordion').find(‘.content').hide(); // تأثير الطّيّ $('#accordion').find('.accordion-header').click(function(){ var next = $(this).next(); next.slideToggle('fast'); $('.content').not(next).slideUp('fast'); return false; });كل ما تحتاجه إلى جانب هذا المقطع هو شفرة HTML المناسبة للتأثير. أولا نغلق كل اللوحات ثم عند حدث النقر click ينزلق المحتوى المربوط بالترويسة accordion-header بالتتابع. هذه طريقة سهلة للحصول على تأثير طي بسرعة. تحديد ارتفاع عنصر div اعتمادا على آخر$('.div').css('min-height', $('.main-div').height());تمكِّن هذه الطريقة من تحديد نفس الارتفاع لعنصريْ div بغض النظر عن محتوى كل منهما. في السطر أعلاه عيّنا ارتفاع عناصر div بحيث يكون لديها على الأقل ارتفاع العنصر main-div. لائحة بألوان مختلفة حسب العناصِر الزوجية والفردية$('li:odd').css('background', '#E8E8E8');يمنح هذا السطر خلفية باللون المحدد للعناصر الفردية من اللائحة؛ مما يمكنك من الحصول على لائحة مخطَّطة - مثل هيئة الحمار الوحشي - عبر إضافة لون خلفية افتراضي في ملف CSS تأخذه العناصر الزوجية من اللائحة. لا يقتصر استخدام هذه الطريقة على اللوائح بل يتعداها إلى الجداول وعناصر div وغيرها. ترجمة بتصرّف لمقال 10 jQuery snippets every designer should know لصاحبته Sara Vieira.
    1 نقطة
  32. تُسهِّل مكتبة jQuery التّعامل مع محتويات صفحة HTML بعد أن يعرضها المتصفّح، وتوفّر أدوات تُساعدك في متابعة تفاعل المستخدم مع الصّفحة، وتحريك العناصر فيها، والتّواصل مع الخواديم دون إعادة تحميل الصّفحة. سنشرح هذه الأدوات بعد قليل. لنبدأ أوّلًا بالاطّلاع على أساسيّات jQuery، وكيف يمكن استخدام وظيفتها الأساسيّة: الوصول إلى عناصر مُحدَّدة في الصفحة وفعل شيءٍ ما بها. ملاحظة: يفترض هذا الدّليل أنّك على علم بأساسيّات HTML ومُحدِّدات CSS. إن لم تكن تألف كيف يمكن استخدام مُحدّدات CSS للوصول إلى عناصر مُحدّدة في الصّفحة، فعليك أوّلًا تعلّم ذلك قبل الشروع في متابعة هذا الدّليل. ما هذا الرمز: $؟توفّر مكتبة jQuery الدّالّة jQuery، التي تتيح لك تحديد العناصر بمُحدّدات CSS. var listItems = jQuery( 'li' );إن قرأت من قبل برامج تستخدم jQuery، فلا بدّ أنّك اعتدت على هذا: var listItems = $( 'li' );كما ناقشنا في الجزء السّابق (أساسيّات JavaScript)، فكلّ الأسماء تكاد تكون سّليمة في JavaScript ما لم تبدأ برقم أو تحوي إشارة "-". ولذا فالاسم $ في المثال الأخير ليس إلّا اسمًا مُختصرًا للدّالّة jQuery، ولو اطّلعت على مصدر jQuery، لقرأت هذا قرب نهايته: // Expose jQuery to the global object window.jQuery = window.$ = jQuery;عندما تستدعي الدّالّة ‎$()‎ وتمرّر لها مُحدّدًا، فإنّك تحصل على كائن jQuery جديدٍ. الدّوالّ في JavaScript هي الأخرى كائنات، وهذا يعني أنّ للدّالّة $ (وjQuery بالطّبع) خصائص ووظائف أيضًا. مثلاً: يمكنك استخدام الخاصّة ‎$.support‎ لمعرفة ما الميزات الّتي يدعمها المتصفّح الحاليّ، كما يمكنك استخدام الوظيفة ‎$.ajax‎ لإرسال طلب AJAX. ملاحظة: من الآن فصاعدًا سنستخدم $ بدلًا من jQuery في هذه السّلسلة سعيًا للاختصار. لاحظ أنّه إن احتوت صفحتك أكثر من مكتبة واحدة، فقد يُستخدم الاسم $ من مكتبة أخرى، ممّا يمنع عمل jQuery، فإن واجهتك مشكلة كهذه، جرّب استخدام jQuery.noConflict قبل تحميل المكتبات الأخرى. ‏‎$(document).ready()‎قبل استخدام jQuery لفعل أيّ شيء في الصّفحة، علينا التأكّد من كون الصّفحة قد بلغت حالةً تسمح بتعديل محتوياتها. يمكن تنفيذ ذلك في jQuery بإحاطة برنامجنا ضمن دالّة ثمّ إمرار هذه الدّالة إلى ‎$(document).ready()‎. كما ترى في المثال التّالي، يمكن للدّالّة الّتي نمرّرها أن تكون مجهولة (بلا اسم): $(document).ready(function() { console.log( 'ready!' ); });هذا سيؤدّي إلى استدعاء الدّالّة الّتي مرّرناها إلى ‎.ready()‎ بعد أن يصبح المُستند (الصفحة) جاهزًا. ما الذي يحدث هنا؟ استخدمنا ‎$(document)‎ لإنشاء كائن jQuery من document في الصّفحة، ثمّ استدعينا الدّالّة ‎.ready()‎ على هذا الكائن، مُمرِّرين إليها الدّالّة الّتي نريد تنفيذها. بما أنّك ستجد نفسك تُعيد كتابة هذا النّصّ مرارًا، فإنّ jQuery تقدّم لك طريقةً مُختصرةً لإنجازه، إذ تقوم الدّالّة ‎$()‎ بمهمّة مُختلفة عند إمرار دالّة إليها بدلًا من مُحدِّد CSS، وعندها تتصرّف وكأنّها اسم بديلٌ للوظيفة ‎$(document).ready()‎: $(function() { console.log( 'ready!' ); });ملاحظة: من الآن فصاعدًا، سنفترض أنّ النّصوص الّتي ترد في هذه السّلسلة مُحاطة بالعبارة ‎$(document).ready(function() { ... });‎، وسنترك هذه العبارة بغرض الإيجاز. الوصول إلى العناصرأبسط ما يمكن إنجازه بـjQuery تحديد بعض العناصر ثمّ فعل شيء ما بها. إن كنت تفهم مُحدّدات CSS، فستجد أنّ الوصول إلى بعض العناصر سهل ومباشر: ليس عليك إلا إمرار المُحدِّد المناسب إلى ‎$()‎. $( '#header' ); // حدّد العنصر الّذي مُعرِّفه 'header' $( 'li' ); // حدّد كل عناصر القوائم في الصّفحة $( 'ul li' ); // حدّد كل عناصر القوائم الموجودة في قوائم غير مُرتّبة $( '.person' ); // حدّد كل العناصر ذات الصّنف 'person'من المهمّ أن تفهم أنّ أيّ تحديد تُجري لن يتضمّن إلّا العناصر الموافقة للمُحدّد والتي كانت موجودة في اللّحظة الّتي أجريت فيها التّحديد، بمعنى أنّك إذا كتبت ‎var anchors = $( 'a' );‎ ثمّ أضفت عنصر <a> إلى الصّفحة لاحقًا، فلن تحوي anchors العنصر الجديد. طرق أخرى لإنشاء كائن jQueryبالإضافة إلى إمرار مُحدّد بسيط إلى الدّالة ‎$()‎، يمكن إنشاء كائنات jQuery بطرق أخرى: // أنشئ كائن jQuery من عنصر DOM $( document.body.children[0] ); // أنشئ كائن jQuery من قائمة بعناصر DOM $( [ window, document ] ); // أجرِ التّحديد بسياق عنصر DOM var firstBodyChild = document.body.children[0]; $( 'li', firstBodyChild ); // أجرِ التّحديد ضمن تحديد سابق var paragraph = $( 'p' ); $( 'a', paragraph );هل يحتوي التّحديد الّذي أجريته على أيّة عناصر؟ترغب أحيانًا في تنفيذ بعض الأوامر عندما يُطابق تحديدك عنصرًا أو أكثر فقط، ولكنّ الدّالّة ‎$()‎ تُعيدُ دومًا كائن jQuery، وهذا الكائن دائمًا صائب (truthy)، ولذا عليك فحص محتوى التّحديد لمعرفة إن كان يحوي أيّة عناصر. تحذير: نصّ برمجيّ غير سليم if ( $( '#nonexistent' ) ) { // خطأ! الأوامر هنا ستُنفَّذ دومًا! } if ( $( '#nonexistent' ).length > 0 ) { // صحيح! لن تُنفّذ الأوامر هنا إلّا إن احتوت الصّفحة على كائن مُعرّفه 'nonexistent' }بإمكاننا اختصار هذا الفحص أكثر بالاعتماد على كون 0 قيمة خاطئة (falsy): if ( $( '#nonexistent' ).length ) { // لن تُنفّذ الأوامر هنا إلّا إن وجد عنصر مُطابق }الوصول إلى عناصر مُفردة من تحديدإن كنت تحتاج التّعامل مع عنصر DOM خام من تحديد، فعليك الوصول إلى هذا العنصر من كائن jQuery. لنفترض مثلًا أنّك تريد الوصول إلى الخاصّة value لكائن <input> مباشرةً، عليك إذن التّعامل مع عنصر DOM الخام: var listItems = $( 'li' ); var rawListItem = listItems[0]; // أو listItems.get( 0 ) var html = rawListItem.innerHTML;لاحظ أنّه ليس بإمكانك استدعاء وظائف jQuery على عناصر DOM الخام، فلن يعمل المثال التّالي: تحذير: نصّ برمجيّ غير سليم var listItems = $( 'li' ); var rawListItem = listItems[0]; var html = rawListItem.html(); // Object #<HTMLInputElement> has no method 'html'إن أردت العمل مع عنصر مُفرد في تحديد وأردت استخدام وظائف jQuery معه، فعليك إنشاء كائن jQuery جديد انطلاقًا منه باستخدام الدّالّة ‎.eq()‎، وإمرار دليل العنصر الّذي تريده: var listItems = $( 'li' ); var secondListItem = listItems.eq( 1 ); secondListItem.remove();جرب المثال في ساحة التّجربة (تأكد من ضغط زر Run with JS في هذا المثال وكل الأمثلة التالية) إنشاء كائنات جديدةللدّالّة $ دورٌ ثالث أخير: إنشاء عناصر جديدة. إن مرّرت قصاصة HTML إلى $()‎، فستُنشئ كائنًا جديدًا في الذّاكرة، بمعنى أنّ الكائن يُنشأ ولكن لا يُضاف إلى الصّفحة إلى أن تفعل ذلك بنفسك. $( '<p>' ); // يُنشئ عنصر <p> بلا محتوى $( '<p>Hello!</p>' ); // يُنشى عنصر <p> فيه نصّ $( '<p class="greet">Hello!</p>' ); // يُنشى عنصر <p> فيه نصّ وله صنفبإمكانك أيضًا إنشا عنصر جديد بإمرار كائنٍ يحوي معلومات تصف كيفيّة إنشاء العنصر: $( '<p>', { html: 'Hello!', 'class': 'greet' });لاحظ أنّه يجب أن نُحيط class بعلامتي اقتباس، لأنّ class كلمة محجوزة في JavaScript، وعدم إحاطتها بالعلامتين سيسبّب وقوع أخطاء في بعض المتصفّحات. راجع وثائق jQuery‏ لتفاصيل الخصائص المختلفة الّتي يمكنك تضمينها في هذا الكائن. سنتعرّف كيف نُضيف العناصر في الصّفحة في الجزء القادم من السّلسلة، الّذي يشرح الانتقال عبر الصّفحة وتعديل محتوياتها. التّعامل مع التحديداتبعد إنشائك كائن jQuery يحوي تحديدًا، فإنّك غالبًا ما تريد فعل شيء ما به، وقبل ذلك عليك أن تتعرّف على أسلوب jQuery في التّعامل مع الكائنات. فحص تحديدبإمكاننا معرفة إن كان تحديد ما يُطابق معايير مُعيّنة باستخدام الوظيفة ‎.is()‎. أكثر الطّرق شيوعًا لاستخدام هذه الوظيفة تزويدها بمُحدِّد كمُعاملٍ مفرد لها، وعندها تُعيد true أو false حسب مُطابقة التّحديد للمُحدِّد: $( 'li' ).eq( 0 ).is( '.special' ); // false $( 'li' ).eq( 1 ).is( '.special' ); // trueبإمكانك تمرير كائن jQuery أيضًا إلى الوظيفة ‎.is()‎، أو حتّى كائن DOM خام، أو حتّى دالّة لإجراء اختبار أكثر تعقيدًا إن لزم. راجع الوثائق لمزيد من التّفاصيل. وظائف القراءة والكتابة والسّرد الضّمنيّبعد عمل التّحديد، تتوفّر وظائف عديدة يمكنك استدعاؤها. تقع هذه الوظائف عمومًا في إحدى مجموعتين: وظائف القراءة (getters) ووظائف الكتابة (setters). فالأولى تعطينا معلومات عن التّحديد، والثّانية تُغيّر التّحديد بشكل من الأشكال. وفي معظم الحالات يقتصر عمل وظائف القراءة على العنصر الأول في التّحديد (‎.text()‎ إحدى استثناءات هذه القاعدة)؛ أمّا وظائف الكتابة فتشمل بعملها كلّ العناصر في التّحديد، مستخدمةً ما يُعرف بالسّرد الضّمنيّ (implicit iteration). معنى السّرد الضّمنيّ أنّ jQuery ستمرّ تلقائيًّا على كلّ العناصر في التّحديد عندما تستدعي وظيفة كتابة على هذا التّحديد، أيّ أنّه ليس عليك استدعاء وظيفة على كلّ عنصر في التّحديد بمفرده عندما تريد فعل شيء على كل العناصر في تحديد واحدٍ، بل اكتفِ باستدعاء هذه الوظيفة على التّحديد نفسه، وستفهم jQuery أنّ عليها تنفيذه على كلّ العناصر في التّحديد. لنفترض أنّنا نريد تغيير نصّ HTML في كل عناصر القوائم في الصّفحة، ولفعل ذلك علينا استخدام الوظيفة ‎.html()‎ الّتي تقوم بتغيير نصّ HTML في كلّ عناصر القوائم المُحدّدة. $( 'li' ).html( 'New HTML' );جرب المثال في ساحة التّجربة بإمكانك أيضًا إمرار دالّة إلى وظائف الكتابة في jQuery، وستُستخدم القيمة المُعادة منها باعتبارها القيمة الجديدة، وتستقبل هذه الدّالة مُعاملين اثنين: دليل العنصر (index) في التّحديد، والقيمة القديمة للشّيء الذي تحاول تغييره، وهذا مُفيد في حال احتجت معلومات عن حالة العنصر الحاليّة لتعيين حالته الجديدة بشكل صحيح. $( 'li' ).html(function( index, oldHtml ) { return oldHtml + '!!!' });جرب المثال في ساحة التّجربة السّرد الصّريح (Explicit Iteration)في بعض الأحيان، لن تلبّي وظائف jQuery الأصليّة المهمّة الّتي تريد إنجازها بدقّة، وسيكون عليك حينها المرور على العناصر في التّحديد بشكل صريح، وهذا ما تتيحه الوظيفة ‎.each()‎. في المثال التّالي نستخدمها لإضافة وسم <b> في بداية عنصر القائمة، يحوي دليل العنصر: $( 'li' ).each(function( index, elem ) { // this: عنصر DOM الخام الحالي // index: دليل العنصر الحالي في التّحديد // elem: عنصر DOM الخام الحالي (مثل this) $( elem ).prepend( '<b>' + index + ': </b>' ); });جرب المثال ساحة التّجربة ملاحظة: ستلاحظ أنّ عنصر DOM الخام مُتاح ضمن الدّالّة الّتي نُمرّرها إلى ‎.each()‎ بطريقتين: الأولى عبر this والثّانية عبر elem. وكما ناقشنا في الجزء السّابق (أساسيّات JavaScript)، فإنّ this كلمة خاصّة في JavaScript تُشير إلى الكائن الّذي يُمثّل سياق الدّالّة الحاليّ. وفي jQuery فإنّ this تُشير في معظم الحالات إلى عنصر DOM الخام الّذي تعمل عليه الدّالّة الحاليّة. لذا فإنّها تُشير في حالة ‎.each()‎ إلى العنصر الحاليّ في مجموعة العناصر الّتي نسردها. السَّلسَلة (Chaining)من أكثر الأمور فائدةً في jQuery إمكانيّة "سَلسَلة" الوظائف معًا. هذا يعني أنّ بإمكاننا استدعاء سلسِلة من الوظائف على تحديدٍ ما دون الحاجة لإعادة التّحديد أو حفظه في متغيّر. بإمكاننا حتّى إنشاء تحديدات جديدة بناء على التّحديد السّابق، دون كسر السّلسلة: $( 'li' ) .click(function() { $( this ).addClass( 'clicked' ); }) .find( 'span' ) .attr( 'title', 'Hover over me' );الأمر ممكن لأنّ كل دالّة كتابة (setter) في jQuery تُعيد التّحديد الذي اُستدعيت لتعمل عليه. وهذا أمر عظيم الفائدة، حتّى أنّ مكتبات كثيرة اعتمدته تأثّرًا بـjQuery. ولكن يجب الحذر عند استخدامه. فالسّلاسل الطّويلة تجعل النّصّ البرمجيّة صعب القراءة والتّعديل والتنقيح لا قاعدة واضحة تفرض طولًا مناسبًا للسّلسلة، ولكن حتّى السلاسل القصيرة قد تحتاج إلى إعادة الصّياغة تسهيلًا لقراءتها. var listItems = $( 'li' ); var spans = listItems.find( 'span' ); listItems .click(function() { $( this ).addClass( 'clicked' ); }); spans.attr( 'title', 'Hover over me' );خاتمةلدينا الآن معلومات ممتازة عن تفاصيل عمل jQuery؛ وسنستعرض في الجزء القادم كيف يمكننا تطبيق هذه المعلومات لإنجاز أشياء حقيقيّة بها! مصادر إضافيةوثائق الواجهة البرمجيّة لـjQueryوثائق المُحدّداتترجمة (بشيء من التصرف) للجزء الثاني من سلسلة  jQuery Fundamentals لمؤلّفتها Rebecca Murphey.
    1 نقطة
×
×
  • أضف...