جوهر أُطُر العمل الأمامية
💡 دليل التعلم: هذه المقالة ستُجيب عن سؤال جوهري — أُطُر العمل الأمامية (Vue وReact وSvelte وغيرها) ماذا تفعل بالضبط؟ إذا كنت قد تعلمت فقط HTML وCSS وبعض JavaScript، فلا مشكلة على الإطلاق، سنبدأ من الصفر.
قبل البدء، تأكد من أنك تعرف المفهومين الأساسيين التاليين. إذا لم تكن متأكدًا، يمكنك مراجعة الفصول المخصصة لهما:
- HTML: الهيكل العظمي لصفحة الويب، يُحدد العناصر الموجودة على الصفحة (عناوين، فقرات، أزرار، صور...). راجع تخطيط HTML وCSS.
- JavaScript: لغة البرمجة التي تجعل صفحة الويب "تتحرك"، يمكنها تعديل محتوى الصفحة والاستجابة لعمليات المستخدم. راجع دليل JavaScript المتعمق.
هناك أيضًا مفهوم سيظهر بشكل متكرر لاحقًا، سنشرحه بالكامل هنا أولًا.
ما هو DOM؟
DOM هو اختصار لـ Document Object Model، أي "نموذج كائن المستند".
عندما تفتح صفحة ويب في المتصفح، أول شيء يفعله المتصفح هو قراءة كود HTML. بعد الانتهاء من القراءة، لا يستخدم المتصفح نص HTML مباشرةً لعرض الصفحة، بل يحوّل كود HTML أولًا إلى بنية شجرية ويخزنها في الذاكرة. هذه الشجرة تُسمى شجرة DOM.
كل عقدة (Node) على الشجرة تقابل علامة HTML. علاقات التعشيق بين العلامات تتحول في شجرة DOM إلى علاقات عقدة أب وعقدة ابن.
👇 جرّب بنفسك: حرّك الماوس فوق كود HTML على اليسار، وستتوهج العقدة المقابلة في شجرة DOM على اليمين. والعكس صحيح أيضًا. كل سطر من علامات HTML يقابل عقدة واحدة في شجرة DOM.
<h1> or <p>, maps to one node.لماذا يجب أن تفهم DOM؟ لأن طريقة JavaScript في تعديل الصفحة هي التعامل مع شجرة DOM هذه — إضافة عقد، حذف عقد، تعديل محتوى العقد. والعمل الأساسي الذي تقوم به أُطُر العمل الأمامية هو أتمتة عمليات DOM هذه نيابةً عنك. سنذكر DOM مرارًا لاحقًا، وفهمه هو الأساس لفهم مبادئ أُطُر العمل.
0. مقدمة: ما هو "إطار العمل الأمامي"؟
دعنا أولًا نشرح كلمة "إطار العمل". في البرمجة، Framework (إطار العمل) هو مجموعة من الكود والقواعد المكتوبة مسبقًا، والتي تحدد كيف يجب تنظيم الكود الخاص بك وكيف يجب أن يعمل. أنت تكتب الكود بطريقته، وهو يتولى عنك الكثير من العمل المتكرر والمعقد في المستوى الأدنى.
إطار العمل الأمامي، هو إطار عمل متخصص في مساعدتك على بناء واجهات صفحات الويب. الأكثر شيوعًا حاليًا هي Vue وReact وSvelte وAngular.
إذن ما هي المشكلة التي تساعدك في حلها؟ البطاقات الثلاث التالية تلخص المنطق الجوهري:
- When data changes, manual DOM updates are easy to miss.
- The more complex a page becomes, the more places need synchronization.
- In team work, scattered DOM operations become hard to maintain.
- The browser does not know the relationship between data and UI.
- Native DOM APIs are low-level operations, not automatic UI synchronization.
- Developers are forced to act as manual synchronizers.
- Build a mapping from data to UI: UI = f(State).
- Detect data changes automatically with reactivity.
- Compute minimal DOM updates with virtual DOM or compiler optimization.
بعد ذلك سنشرح خطوة بخطوة، بدءًا من المسألة الأساسية.
1. المشكلة الجوهرية: البيانات تغيرت، ماذا عن الواجهة؟
1.1 أوضح أولًا ما هي "البيانات" وما هي "الواجهة"
في أي تطبيق ويب، يوجد شيئان معًا في نفس الوقت:
- البيانات (Data / State): المعلومات المخزنة داخل البرنامج. مثل "في سلة التسوق 3 منتجات" و"اسم المستخدم هو أحمد" و"التبويب المحدد حاليًا هو الثاني". هذه البيانات مخزنة في متغيرات JavaScript، والمستخدم لا يراها.
- الواجهة (UI): ما يراه المستخدم على الشاشة. مثل عرض "سلة التسوق(3)" على الصفحة، وعرض "مرحبًا، أحمد"، والتبويب الثاني مُظلل. هذه هي التأثيرات البصرية التي تقدمها عناصر HTML.
توجد علاقة تطابق بين البيانات والواجهة: البيانات هي "3 منتجات"، والواجهة يجب أن تعرض "3". إذا أصبحت البيانات "4 منتجات"، يجب أن تتغير الواجهة أيضًا إلى "4".
المشكلة هي: من المسؤول عن عملية "التغير معًا" هذه؟
👇 جرّب النقر: انقر على زر "إضافة منتج"، ولاحظ أن البيانات (على اليسار) قد تغيرت، لكن الواجهة (على اليمين) لم تتبع التحديث — أصبحت "منفصلة" بينهما. انقر "مزامنة الواجهة" للإصلاح يدويًا.
1.2 لماذا عندما يتغير متغير JavaScript، لا تتحدث الواجهة تلقائيًا؟
هذه أكثر نقطة تُربك المبتدئين، سنشرح المبدأ الأساسي خطوة بخطوة.
في JavaScript، المتغير هو ببساطة مساحة في الذاكرة تُستخدم لتخزين البيانات. عندما تنفذ count = count + 1، ما يفعله محرك JavaScript بسيط جدًا: يغيّر القيمة في موقع count في الذاكرة من 3 إلى 4. بعد هذه الخطوة ينتهي كل شيء، ولن يحدث أي شيء آخر.
أما المحتوى المعروض على الصفحة (مثل عقدة DOM <span>3</span>) فمخزن في مساحة ذاكرة مختلفة تمامًا. عندما يُعدّل محرك JavaScript المتغير، فإنه لا يعرف أصلًا أن هناك عقدة DOM على الصفحة تعرض قيمة هذا المتغير، وليست هناك أي آلية تجعله يتحقق.
لذلك السبب الجوهري هو: متغيرات JavaScript وعُقد DOM كتلتا ذاكرة مستقلتان، لا توجد بينهما أي آلية ربط تلقائي. تعديل المتغير يغيّر فقط الذاكرة التي يحتلها المتغير، بينما الذاكرة التي تحتلها عقدة DOM لا تتأثر إطلاقًا.
let count = 3
// على الصفحة عقدة DOM تعرض قيمة count:
// <span id="counter">3</span>
count = 4
// ماذا فعل محرك JavaScript؟
// → غيّر قيمة المتغير count في الذاكرة من 3 إلى 4
// → انتهى. فقط.
// ما زال <span> على الصفحة يعرض "3"إذا أردت أن ما يظهر على الصفحة يتغير أيضًا إلى "4"، يجب عليك كتابة كود إضافي، والعثور يدويًا على عقدة DOM تلك، ثم تعديل محتواها:
count = 4 // الخطوة 1: تغيير المتغير
// الخطوة 2: يجب أن تكتب بنفسك — ابحث عن عقدة DOM وغيّر نصها إلى القيمة الجديدة
document.getElementById('counter').textContent = countإذا كان على الصفحة 5 أماكن تعرض قيمة count (عدد سلة التسوق، قائمة المنتجات، السعر الإجمالي، المجموع الفرعي، رسالة الحالة)، ستحتاج إلى كتابة 5 أجزاء من هذا الكود. إذا فاتك أي جزء، فذلك المكان سيظل يعرض القيمة القديمة، والمستخدم سيرى معلومات خاطئة.
1.3 ماذا يفعل إطار العمل؟ خطوتان لبناء اتصال تلقائي
يستطيع إطار العمل المزامنة التلقائية بفضل تنسيق خطوتين — لا غنى عن إحداهما.
الخطوة الأولى: أنت "تُسجّل" في القالب الأماكن التي تريد عرض المتغير فيها
في قالب HTML الخاص بإطار العمل، تستخدم صيغة مثل لتحديد "هذا المكان يريد عرض قيمة count":
<!-- قالب Vue -->
<span>سلة التسوق: {{ count }} منتجات</span> <!-- الموقع أ: أريد عرض count -->
<span>السعر الإجمالي: ¥{{ count * 99 }}</span> <!-- الموقع ب: أنا أيضًا أستخدم count -->
<span>{{ count > 5 ? 'كثير جدًا' : 'طبيعي' }}</span> <!-- الموقع ج: أنا أيضًا أستخدم count -->عندما يعرض إطار العمل الصفحة لأول مرة، يُسجّل "علاقة التسجيل" هذه: المواقع أ وب وج تعتمد جميعها على count.
الخطوة الثانية: يراقب إطار العمل المتغير، وعندما يتغير يفحص جدول التسجيل ويُحدّث تلقائيًا
يستخدم إطار العمل آلية Proxy (الوكيل) المدمجة في JavaScript ليُغلف متغيرك، ويحوّله إلى "متغير مراقَب". عندما تُعدّل هذا المتغير، يقوم Proxy بأن يفعل شيئًا إضافيًا بصمت أثناء الإسناد: يُخطر إطار العمل بأن "count قد تغيّر". بعد أن يتلقى إطار العمل الإخطار، يذهب لفحص جدول التسجيل من الخطوة الأولى، ويُحدّث المواقع الثلاثة أ وب وج.
JS الأصلي:
أنت تكتب HTML → <span id="counter">3</span> (بدون أي اتصال مع المتغير)
أنت تغيّر المتغير → count = 4 → انتهى، الواجهة بلا أي استجابة
أنت تُكمّل يدويًا → document.getElementById('counter').textContent = 4 → الواجهة تتحدث فقط
إطار العمل Vue:
أنت تكتب القالب → <span>{{ count }}</span> (إطار العمل يتذكر: هذا المكان يعتمد على count)
أنت تغيّر المتغير → count = 4 → Proxy يعترض → يُخطر إطار العمل → إطار العمل يفحص جدول التسجيل → يُحدّث تلقائيًا أ/ب/جهذا هو سبب أن "فقط إطار العمل يمكنه المزامنة تلقائيًا" — <span> في HTML الأصلي ومتغير JS ليس بينهما أي اتصال أصلًا، صيغة القالب () الخاصة بإطار العمل هي المفتاح لبناء هذا الاتصال. أنت تكتب ، فيعرف إطار العمل أن هذا المكان يجب أن يعرض count؛ وعندها فقط يستطيع إطار العمل عند تغير count أن يحدد هذا الموقع بدقة ويُحدّثه.
👇 جرّب النقر: اختر أولًا "JavaScript الأصلي"، بعد النقر على "تنفيذ" لاحظ — المتغير تغيّر لكن الواجهة ثابتة، عليك أن تُزامن كل موقع خطوة بخطوة يدويًا. ثم انتقل إلى "استخدام إطار العمل"، وانقر "تنفيذ" أيضًا — بمجرد تغير المتغير، يُكمل إطار العمل جميع الخطوات تلقائيًا، والواجهة تتبع فورًا.
count = 4, the engine only changes the value in memory. It does not notify anyone, trigger a callback, or check where count is displayed on the page. The UI will not change unless you update the DOM yourself.1.4 مقارنة: تأثير المزامنة اليدوية مقابل المزامنة التلقائية
بعد فهم المبدأ، دعنا نرى في سيناريو أكثر تعقيدًا قليلًا، كم هو كبير الفرق بين المزامنة اليدوية والمزامنة التلقائية.
👇 جرّب النقر: على اليسار طريقة "المزامنة اليدوية" بدون إطار عمل — كل منطقة عرض تحتاج أن تنقر زر "مزامنة" بشكل منفصل للتحديث. على اليمين طريقة "المزامنة التلقائية" مع إطار عمل — أنت فقط انقر "إضافة منتج"، وجميع مناطق العرض تتحدث تلقائيًا. جرّب أن تتعمد عدم مزامنة منطقة معينة على اليسار، وانظر ماذا سيحدث.
هذا هو السبب الجوهري لوجود أُطُر العمل الأمامية: إضافة قدرة "الإخطار التلقائي للواجهة بالتحديث عند التعديل" إلى متغيرات JavaScript، والقضاء على الأخطاء الناتجة عن المزامنة اليدوية.
2. الفكرة الجوهرية لإطار العمل: وصف الواجهة بالبيانات
2.1 الفرق بين طريقتي الكتابة
بعد فهم قيمة "المزامنة التلقائية"، دعنا نرى كيف يُنفذ إطار العمل ذلك تحديدًا.
في حقبة ما قبل أُطُر العمل (مثل استخدام jQuery)، كان الكود يُكتب هكذا — أنت تُخبر المتصفح خطوة بخطوة ماذا يفعل:
// الخطوة 1: ابحث عن العنصر الذي يحمل id يساوي counter على الصفحة
var element = document.getElementById('counter')
// الخطوة 2: غيّر المحتوى النصي لهذا العنصر إلى القيمة الجديدة
element.textContent = '4'
// الخطوة 3: ابحث عن عنصر آخر، وغيّره أيضًا
document.getElementById('total').textContent = '¥396'
// الخطوة 4: إذا كانت الكمية أكبر من 5، يجب أيضًا تغيير رسالة الحالة...هذه الطريقة في الكتابة تُسمى Imperative (أمري) — أنت "تأمر" المتصفح بتنفيذ العمليات خطوة بخطوة.
مع وجود إطار العمل، يصبح الكود هكذا — أنت فقط تصف "كيف يجب أن تبدو الواجهة":
<!-- لا يهمني كيف تُحدَّث هذه القيمة على الصفحة -->
<!-- أنا فقط أقول: هذا المكان يجب أن يعرض قيمة count -->
<span>{{ count }}</span>
<span>السعر الإجمالي: ¥{{ count * 99 }}</span>
<span v-if="count > 5">المنتجات كثيرة جدًا!</span>هذه الطريقة في الكتابة تُسمى Declarative (تقريري) — أنت "تُصرّح" بالحالة النهائية للواجهة، أما كيفية الوصول إلى هذه الحالة فيتولاه إطار العمل بنفسه.
2.2 الصيغة الجوهرية: UI = f(State)
جميع أُطُر العمل الأمامية الحديثة — سواء Vue أو React أو Svelte — تتبع نفس الفكرة الجوهرية، والتي يمكن التعبير عنها بصيغة واحدة:
UI = f(State)
هذه الصيغة تعني:
- State (الحالة): بيانات تطبيقك. إنها ببساطة تلك المتغيرات في JavaScript: كم عدد المنتجات في سلة التسوق، هل سجّل المستخدم الدخول، ما هي الصفحة الحالية...
- f (الدالة): آلية العرض الخاصة بإطار العمل. إنها تعرف كيف تحوّل البيانات إلى واجهة.
- UI (الواجهة): النتيجة النهائية التي يراها المستخدم على الشاشة.
المعنى: بمجموعة بيانات مُعطاة (State)، وبعد معالجة إطار العمل (f)، يمكن الحصول بشكل حتمي على الواجهة المقابلة (UI). البيانات تتغير، فتتغير الواجهة تبعًا لذلك. يحتاج المطور فقط إلى الاهتمام بالبيانات، ولا يحتاج إلى الاهتمام بكيفية تحديث الواجهة.
👇 جرّب النقر: عدّل البيانات (State) على اليسار، ولاحظ كيف تتغير الواجهة (UI) على اليمين تلقائيًا. هذا هو التجسيد المرئي لـ UI = f(State).
{
"username": "(empty)",
"count": 2,
"darkMode": false
}2.3 لماذا Declarative أفضل من Imperative؟
تكمن مزايا طريقة الكتابة Declarative في:
| بُعد المقارنة | Imperative (بدون إطار عمل) | Declarative (مع إطار عمل) |
|---|---|---|
| حجم الكود | كل تحديث يحتاج كتابة كود عمليات محدد | تكتب القالب مرة واحدة فقط، وإطار العمل يعالج الباقي تلقائيًا |
| احتمال الخطأ | من السهل تفويت تحديث مكان ما | إطار العمل يضمن تحديث جميع الأماكن |
| سهولة القراءة | الكود ممزوج بعمليات DOM كثيرة | الكود يصف بوضوح بنية الواجهة |
| تكلفة الصيانة | تعديل ميزة واحدة يتطلب تغيير أماكن كثيرة | يكفي تعديل منطق البيانات، والواجهة تتبع تلقائيًا |
ببساطة: Declarative تتيح لك التركيز على "منطق الأعمال" (كيف تتغير البيانات)، دون القلق بشأن "كيف تُحدَّث الواجهة" — هذا العمل المتكرر والمعرّض للأخطاء.
3. نظام التفاعلية: كيف يعرف إطار العمل أن البيانات تغيرت؟
3.1 ما هي "التفاعلية"؟
سابقًا ذكرنا أن "البيانات تتغير، والواجهة تُحدَّث تلقائيًا". لكن هناك مشكلة تقنية: JavaScript بحد ذاته لا يمتلك قدرة "الإخطار التلقائي للآخرين عند تعديل المتغيرات".
أنت تكتب count = 4، فيغيّر JavaScript قيمة count من 3 إلى 4 فقط، ولن يُخطر أي شخص تلقائيًا. إطار العمل يحتاج إلى آلية "لاكتشاف" أنك عدّلت البيانات.
Reactivity (التفاعلية) هي الاسم العام لهذه الآلية: عندما تتغير البيانات، يستطيع النظام الإحساس بالتغيير تلقائيًا وتنفيذ عمليات التحديث المقابلة.
3.2 ثلاث طرق تنفيذ مختلفة
تتبنى أُطُر العمل المختلفة حلولًا تقنية مختلفة لتنفيذ التفاعلية. وهذا أيضًا هو الاختلاف الأكثر جوهرية بين Vue وReact وSvelte.
الطريقة الأولى: اعتراض الوكيل (طريقة Vue)
يستخدم Vue آلية Proxy (الوكيل) المدمجة في JavaScript. يمكن لـ Proxy أن ينفذ تلقائيًا كودًا تحدده أنت عند قراءة أو تعديل خاصية من خصائص كائن.
يُغلف Vue كائن البيانات الخاص بك باستخدام Proxy. عندما تنفذ count = 4، يعترض Proxy عملية الكتابة هذه، ويُخطر Vue بأن "قيمة count قد تغيرت"، ثم يذهب Vue لتحديث جميع أجزاء الواجهة التي تستخدم count.
أنت كمطور لا تحتاج إلى فعل أي شيء إضافي — فقط أسند القيمة مباشرة، وVue يحس بالتغيير تلقائيًا.
الطريقة الثانية: الاستدعاء الصريح (طريقة React)
React لا يستخدم Proxy. بل يتطلب منك تعديل البيانات من خلال دالة متخصصة:
// طريقة كتابة React
const [count, setCount] = useState(0)
// لا يمكنك كتابة count = 4 مباشرة (React لن يشعر بذلك)
// يجب استدعاء setCount:
setCount(4)فقط عندما تستدعي setCount() يعلم React أن البيانات قد تغيرت، ويذهب لتحديث الواجهة. إذا كتبت مباشرة count = 4، فلن يعلم React أصلًا، ولن تتحدث الواجهة.
هذه الطريقة أكثر "صراحة" — كل تغيير في البيانات تُخطر به إطار العمل بنفسك بشكل نشط، ولن تكون هناك تحديثات غير متوقعة.
الطريقة الثالثة: تحليل المُترجم (طريقة Svelte)
اتخذ Svelte مسارًا مختلفًا تمامًا. لديه مُترجم (Compiler)، وقبل تشغيل كودك، يحلل المُترجم الكود المصدري الخاص بك.
عندما يرى المُترجم أنك كتبت عبارة إسناد مثل count += 1، فإنه يُدرج تلقائيًا بعد هذا السطر كود "إخطار الواجهة بالتحديث". أي أنه عند تشغيل الكود، يكون فعل "الإخطار" قد رتّبه المُترجم مسبقًا.
يبدو كودك كإسناد JavaScript عادي، لكن في الكود المُترجم أُضيف منطق تحديث الواجهة.
👇 جرّب النقر: اختر تبويبات أُطُر العمل المختلفة، انقر "تعديل البيانات"، ولاحظ الخطوات التي يمر بها كل إطار عمل "تحت الغطاء" لإكمال اكتشاف تغير البيانات وتحديث الواجهة.
3.3 مقارنة بين الطرق الثلاث
| بُعد المقارنة | Vue (Proxy الوكيل) | React (الاستدعاء الصريح) | Svelte (المُترجم) |
|---|---|---|---|
| طريقة كتابة المطور | إسناد مباشر count = 4 | يجب استخدام setCount(4) | إسناد مباشر count = 4 |
| توقيت الإحساس بالتغيير | اعتراض تلقائي وقت التشغيل | المطور يُخطر بشكل نشط | إدراج كود الإخطار وقت الترجمة |
| تكلفة الأداء وقت التشغيل | Proxy له تكلفة اعتراض ضئيلة | جدولة setState لها تكلفة ضئيلة | تكاد لا تكون هناك تكلفة إضافية |
| صعوبة التنقيح | متوسطة | تدفق البيانات واضح، أسهل نسبيًا | يحتاج لفهم الكود المُترجم |
| السيناريو المناسب | السعي لكفاءة التطوير وطريقة كتابة طبيعية | السعي لتدفق بيانات يمكن التنبؤ به | السعي لأقصى أداء تشغيلي |
الطرق الثلاث ليس فيها صواب وخطأ مطلق. Vue يُكتب بشكل أكثر طبيعية، وReact لديه تدفق بيانات أكثر تحكمًا، وSvelte لديه أفضل أداء تشغيلي. اختيار أي منها يعتمد على متطلبات المشروع المحددة.
4. المكونات: تقسيم الواجهة إلى كتل صغيرة قابلة لإعادة الاستخدام
4.1 لماذا نقسم؟
قد تحتوي صفحة ويب كاملة على شريط تنقل، شريط جانبي، منطقة محتوى، مربع بحث، صورة المستخدم، أزرار متنوعة... إذا كُتبت جميع الأكواد في ملف واحد، سيصبح هذا الملف طويلًا جدًا وصعب الصيانة جدًا.
المكون (Component) هو تقسيم الواجهة إلى كتل صغيرة مستقلة، كل كتلة تدير بياناتها الخاصة، وواجهتها الخاصة، ومنطقها الخاص.
على سبيل المثال، يمكن تقسيم صفحة تجارة إلكترونية إلى هذه المكونات:
- مكون
NavBar: مسؤول عن شريط التنقل العلوي - مكون
SearchBox: مسؤول عن مربع البحث - مكون
ProductCard: مسؤول عن بطاقة منتج واحدة - مكون
ShoppingCart: مسؤول عن سلة التسوق
كل مكون مستقل. ProductCard لا يحتاج إلى معرفة ماذا كُتب في كود NavBar، هو فقط يدير نفسه.
4.2 ثلاث فوائد للمكونات
الفائدة الأولى: إعادة الاستخدام. بعد كتابة مكون ProductCard واحد، يمكن استخدامه 100 مرة على الصفحة — في كل مرة تُمرر بيانات منتج مختلفة، سيتم عرض بطاقة منتج مختلفة. لا حاجة لنسخ ولصق 100 نسخة من كود HTML.
الفائدة الثانية: التغليف. البيانات والمنطق داخل المكون مستقلان. تعديل كود مكون SearchBox لن يؤثر على مكون ProductCard. عند العمل الجماعي، يمكن لأشخاص مختلفين تطوير مكونات مختلفة في نفس الوقت دون التداخل مع بعضهم.
الفائدة الثالثة: القابلية للصيانة. عندما تظهر مشكلة في ميزة معينة، يمكنك تحديد المكون المقابل مباشرة للإصلاح، دون الحاجة للبحث في ملف كبير من عدة آلاف من الأسطر.
👇 جرّب النقر: انقر على اسم المكون على اليسار، لرؤية المنطقة المقابلة له على الصفحة. لاحظ أن مكون ProductCard نفسه أُعيد استخدامه عدة مرات، كل مرة يعرض بيانات مختلفة.
4.3 كيف يبدو المكون في الكود؟
باستخدام Vue كمثال، المكون هو ملف .vue واحد، يحتوي على ثلاثة أجزاء:
<!-- ProductCard.vue -->
<template>
<!-- هنا تُكتب بنية HTML — "المظهر" للمكون -->
<div class="card">
<h3>{{ name }}</h3>
<p>السعر: ¥{{ price }}</p>
<button @click="addToCart">أضف إلى السلة</button>
</div>
</template>
<script setup>
// هنا يُكتب منطق JavaScript — "السلوك" للمكون
const props = defineProps(['name', 'price'])
function addToCart() {
// معالجة منطق "إضافة إلى سلة التسوق"
}
</script>
<style scoped>
/* هنا تُكتب أنماط CSS — "التصميم" للمكون */
.card {
border: 1px solid #ccc;
padding: 16px;
}
</style>عند استخدام هذا المكون، يكون الأمر كاستخدام علامة HTML مخصصة:
<!-- استخدام مكون ProductCard في مكان آخر -->
<ProductCard name="سماعات لاسلكية" price="299" />
<ProductCard name="لوحة مفاتيح ميكانيكية" price="599" />
<ProductCard name="شاشة عرض" price="1999" />ثلاثة أسطر من الكود تعرض ثلاث بطاقات منتجات مختلفة.
5. تكلفة عمليات DOM: لماذا يبذل إطار العمل كل هذا الجهد؟
5.1 ما هي عملية DOM؟
سبق أن ذكرنا DOM — البنية الشجرية التي يُنشئها المتصفح بعد تحليل HTML. عملية DOM هي استخدام JavaScript لتعديل العُقد على هذه الشجرة. مثل تغيير نص، أو إضافة عنصر، أو حذف عنصر، أو تعديل نمط.
هذه العمليات بحد ذاتها ليست معقدة، لكن بعد تنفيذ عملية DOM، يحتاج المتصفح إلى القيام بالكثير من العمل الإضافي كي يُحدَّث العرض على الشاشة:
- إعادة حساب الأنماط: هل تحتاج أنماط CSS لهذه العقدة والعقد التابعة لها إلى تغيير؟
- إعادة التخطيط (Layout / Reflow): إعادة حساب مواقع وأحجام جميع العناصر على الصفحة. لأن تغيير عنصر واحد قد يؤثر على مواقع عناصر أخرى.
- إعادة الرسم (Paint): رسم المحتوى المُحسَب على الشاشة.
كل خطوة من الخطوات الثلاث لها تكلفة حسابية. إذا كان كودك يُشغّل عمليات DOM بشكل متكرر، فسيتكرر المتصفح في تنفيذ هذه الخطوات، وستصبح الصفحة بطيئة ومتقطعة.
👇 جرّب بنفسك: لاحظ مقارنة الوقت بين التعامل المباشر مع DOM والتعامل الدفعي. عندما يزداد عدد التعديلات، سيرتفع وقت "العمليات الفردية" بشكل حاد.
5.2 كيف يحل إطار العمل هذه المشكلة؟
بما أن التعامل المباشر مع DOM مكلف، يسعى إطار العمل إلى تقليل عدد عمليات DOM. هناك استراتيجيتان محددتان:
الاستراتيجية الأولى: DOM الافتراضي + مقارنة الفروق (طريقة Vue وReact)
DOM الافتراضي (Virtual DOM) هو كائن JavaScript، بنيته تتوافق واحد لواحد مع شجرة DOM الحقيقية، لكنه موجود فقط في الذاكرة، ولا يُشغّل التخطيط والرسم في المتصفح.
عندما تتغير البيانات، يكون تدفق معالجة إطار العمل:
- استخدام كائن JavaScript لإنشاء "شجرة DOM افتراضية جديدة"، تصف كيف يجب أن تبدو الواجهة بعد تغير البيانات
- مقارنة الشجرة الجديدة بالشجرة القديمة (هذه العملية تُسمى Diff، أي مقارنة الفروق)، لمعرفة أي العُقد قد تغيرت
- تطبيق الجزء الذي تغير فعلًا على DOM الحقيقي فقط (هذه العملية تُسمى Patch، أي الترقيع)
بهذه الطريقة، مهما تغيرت البيانات، فإن العمليات على DOM الحقيقي تكون دائمًا في حدها الأدنى.
👇 جرّب النقر: انقر "تعديل البيانات"، ولاحظ كيف يقارن DOM الافتراضي بين الشجرتين الجديدة والقديمة لمعرفة العُقد المتغيرة. انتبه لـ "DOM الحقيقي" في أقصى اليمين — فقط الجزء الذي تغير فعلًا سيومض.
- Learn Vue
- Do homework
- Play games
الاستراتيجية الثانية: التحديد الدقيق وقت الترجمة (طريقة Svelte)
Svelte لا يستخدم DOM الافتراضي. يقوم مُترجمه بالتحليل مسبقًا عند كتابتك للكود: "عندما يتغير count، يجب تحديث عنصر <span> في السطر 3". وقت التشغيل يحدد ذلك العنصر مباشرة للتحديث، دون الحاجة أصلًا لمقارنة الشجرتين الجديدة والقديمة.
هذه الطريقة تتخطى خطوة Diff، وأداؤها أفضل نظريًا. لكنها تعتمد على قدرة المُترجم على التحليل — يحتاج المُترجم إلى أن يكون ذكيًا بما يكفي لتحديد جميع الأماكن التي تحتاج تحديثًا بشكل صحيح.
6. وقت التشغيل مقابل وقت الترجمة: المقايضة الجوهرية في تصميم أُطُر العمل
6.2 مرحلتان
من كتابتك للكود الأمامي حتى تشغله في المتصفح في النهاية، يمر بمرحلتين:
- وقت الترجمة (Compile-time / Build-time): يُعالَج كودك المصدري بواسطة أدوات البناء (مثل Vite وWebpack)، ويُحوَّل إلى كود يمكن للمتصفح تنفيذه مباشرة. هذه العملية تحدث على جهازك، قبل أن يفتح المستخدم صفحة الويب.
- وقت التشغيل (Runtime): يُنفَّذ الكود المُحوَّل في متصفح المستخدم. المنطق الأساسي لإطار العمل (مثل Diff للـ DOM الافتراضي، وتتبع التفاعلية) يعمل في هذه المرحلة.
6.2 توزيع عمل إطار العمل في المرحلتين
تختلف أُطُر العمل المختلفة في حجم العمل المُوزَّع في المرحلتين، مما يحدد خصائص الأداء وحجم الحزمة:
- React: معظم العمل يُنجَز في وقت التشغيل. إنشاء DOM الافتراضي وDiff وPatch تحدث كلها في المتصفح. الميزة هي المرونة العالية؛ والثمن هو ضرورة إرسال كود وقت التشغيل الكامل لإطار العمل (حوالي 40 كيلوبايت) إلى المتصفح.
- Vue: طريقة مختلطة. يُحسَّن القالب وقت الترجمة (يُعلِّم المُترجم أي العُقد ثابتة ولن تتغير)، لكن تحديث الواجهة النهائي لا يزال يتم عبر DOM الافتراضي وقت التشغيل. حجم كود وقت التشغيل حوالي 30 كيلوبايت.
- Svelte: معظم العمل يُنجَز وقت الترجمة. يُحلل المُترجم كودك، ويُنشئ مباشرة تعليمات تحديث DOM دقيقة. وقت التشغيل تكاد لا تكون هناك أكواد إطار عمل — الحزمة الناتجة تحتوي فقط على كود أعمالك الخاص. حجم الحزمة هو الأصغر.
👇 جرّب النقر: انقر على تبويبات أُطُر العمل المختلفة، لرؤية مواقعها على الطيف "وقت التشغيل ↔ وقت الترجمة"، والمقايضات في حجم الحزمة وأداء التشغيل وتجربة التطوير.
6.3 اتجاه الصناعة
اتجاه تطور أُطُر العمل في السنوات الأخيرة واضح جدًا: نقل المزيد والمزيد من العمل من وقت التشغيل إلى وقت الترجمة. لأن حسابات وقت الترجمة لا تستهلك موارد جهاز المستخدم، ولا تؤثر على سرعة تحميل الصفحة.
- Vue يُطوّر حاليًا وضع Vapor، الذي يمكنه تخطي DOM الافتراضي، وإنشاء كود عمليات DOM مباشرة وقت الترجمة
- React أطلق React Compiler، الذي يُحسِّن تلقائيًا سلوك إعادة العرض للمكونات وقت الترجمة
- Svelte 5 قدّم نظام Runes، لتعزيز قدرة التحليل وقت الترجمة بشكل أكبر
7. الخلاصة
دعنا نراجع النقاط الجوهرية في هذه المقالة:
المشكلة الأساسية التي تحلها أُطُر العمل الأمامية: عندما تتغير البيانات في التطبيق، يتم تحديث الواجهة تلقائيًا وبكفاءة وموثوقية، دون حاجة المطور إلى التعامل مع DOM يدويًا.
الفكرة الجوهرية المشتركة: UI = f(State) — الواجهة هي دالة للبيانات، يحتاج المطور فقط إلى الاهتمام بتغير البيانات، وإطار العمل مسؤول عن عكس تغير البيانات على الواجهة.
الفروقات التقنية الرئيسية بينها:
| النقطة التقنية | المعنى |
|---|---|
| نظام التفاعلية | كيف يكتشف إطار العمل تغير البيانات. Vue يستخدم اعتراض Proxy، وReact يستخدم setState الصريح، وSvelte يستخدم تحليل المُترجم. |
| DOM الافتراضي | Vue وReact يستخدمان كائن JavaScript لمحاكاة شجرة DOM، ومن خلال مقارنة الشجرتين الجديدة والقديمة (Diff) لإيجاد الحد الأدنى من التحديث، وتقليل عمليات DOM الحقيقية. |
| المكونات | تقسيم الواجهة إلى كتل مستقلة وقابلة لإعادة الاستخدام، كل مكون يدير بياناته وواجهته الخاصة. |
| تحسين وقت الترجمة | التحليل والتحسين المسبق في مرحلة البناء، لتقليل حجم الحسابات وقت التشغيل. Svelte هو الأبعد في هذا الجانب. |
بجملة واحدة: العمل الجوهري لأُطُر العمل الأمامية هو — تولي عملية المزامنة "من البيانات إلى الواجهة"، بحيث يحتاج المطور فقط إلى التفكير في منطق البيانات، دون الحاجة للتعامل مع الواجهة يدويًا.
جدول المصطلحات
| المصطلح الإنجليزي | المقابل العربي | الشرح |
|---|---|---|
| Framework | إطار العمل | مجموعة من الكود والقواعد المكتوبة مسبقًا، توفر للمطور البنية الأساسية للتطبيق والوظائف الشائعة. |
| DOM | نموذج كائن المستند | بنية بيانات شجرية يُنشئها المتصفح بعد تحليل HTML، يتعامل JavaScript معها لتعديل الصفحة. |
| Virtual DOM | DOM الافتراضي | استخدام كائن JavaScript لمحاكاة شجرة DOM، ومن خلال خوارزمية Diff لإيجاد مسار التحديث الأصغر، وتقليل عدد عمليات DOM الحقيقية. |
| State | الحالة | البيانات في التطبيق، مثل معلومات المستخدم، محتويات سلة التسوق، الحالة الحالية للصفحة وغيرها. |
| Reactivity | التفاعلية | عندما تتغير البيانات، يستطيع النظام الإحساس بالتغيير تلقائيًا وتنفيذ عمليات تحديث الواجهة المقابلة. |
| Proxy | الوكيل | آلية مدمجة في JavaScript، يمكنها اعتراض عمليات القراءة والكتابة على كائن. يستخدمها Vue 3 لتنفيذ التفاعلية. |
| Component | المكون | قطعة كود واجهة مستقلة وقابلة لإعادة الاستخدام، تحتوي على بنية HTML الخاصة بها ومنطق JavaScript وأنماط CSS. |
| Declarative | تقريري | أسلوب برمجة: أنت تصف "النتيجة النهائية التي تريدها"، ويُقرر إطار العمل كيفية التنفيذ. |
| Imperative | أمري | أسلوب برمجة: أنت تُخبر البرنامج خطوة بخطوة "ماذا يفعل تحديدًا". |
| Diff | مقارنة الفروق | مقارنة شجرتَي DOM الافتراضي الجديدة والقديمة، لمعرفة أي العُقد قد تغيرت. |
| Patch | الترقيع | تطبيق التغييرات التي وجدتها Diff على DOM الحقيقي. |
| Compile-time | وقت الترجمة | الفترة التي يُعالَج فيها الكود في مرحلة البناء، وتحدث قبل أن يفتح المستخدم صفحة الويب. |
| Runtime | وقت التشغيل | الفترة التي يُنفَّذ فيها الكود في متصفح المستخدم. |
| Compiler | المُترجم | برنامج يحوّل الكود المصدري إلى شكل آخر من الكود. مُترجم Svelte يحوّل ملفات .svelte إلى JavaScript عالي الكفاءة. |