مقارنة عملية تكشف كيف يؤثر اختيارك بين Composition API وOptions API في Vue 3 على أداء التطبيق، قابلية الصيانة، والقدرة على التوسع — مع أمثلة واقعية من مشاريع الإنتاج.
في أحد أيام الجافاسكريبت الحارة، كنت أراجع كوداً لإحدى الشركات الناشئة التي تعمل على منصة SaaS معقدة. كان الملف مكوناً من ٨٠٠ سطر من كود Vue يستخدم Options API، وكلما حاولت تتبع تدفق البيانات بين الـ methods والـ computed والـ watch، شعرت وكأنني أتنقل بين طبقات البصلة دون أن أصل إلى النواة. المشكلة لم تكن في منطق الكود نفسه، بل في الطريقة التي كان منظم بها: الـ reactivity كان يعمل، ولكن فهم العلاقة بين الـ state والـ side effects كان أشبه بحل لغز دون خريطة. هنا بدأت أفكر بجدية: هل Options API هو حقاً أفضل خيار للمشاريع الكبيرة والمعقدة؟ أم أن Composition API جاء ليحل هذه الفوضى؟
الجدل بين Composition API وOptions API في Vue 3 ليس مجرد مسألة تفضيل شخصي، بل هو قرار يؤثر بشكل مباشر على كيفية إدارة الـ state، تنظيم الـ logic، والتعامل مع الـ side effects في تطبيقك. في هذا المقال، سنفكك الفروقات التقنية بين الاثنين، ونرى كيف يتعامل كل منهما مع الـ reactivity خلف الكواليس، وكيف يؤثر ذلك على أداء التطبيق وقابليته للصيانة. سنستخدم أمثلة واقعية من مشاريع إنتاج، ونناقش الفخاخ الشائعة التي يقع فيها المطورون عند استخدام كل منهما. الهدف ليس فقط أن تعرف الفرق، بل أن تفهم متى يخنق كل منهما الكود الخاص بك ومتى يطلق له العنان.
لفهم الفرق الحقيقي بين Composition API وOptions API، يجب أن نغوص في كيفية تعامل Vue مع الـ reactivity. في Options API، كل خاصية في الـ data، الـ computed، والـ methods تُعامل كخاصية مستقلة في الـ instance الخاص بالـ component. عندما تقوم Vue بتهيئة الـ component، تقوم بإنشاء proxy حول الـ data object، وتراقب التغييرات باستخدام الـ getters والـ setters. هذا يعني أن كل مرة تقوم فيها بتحديث قيمة في الـ data، تقوم Vue بتشغيل الـ reactivity system لتحديث الـ DOM والعناصر المعتمدة على هذه القيمة.
لكن المشكلة تظهر عندما يكون لديك منطق معقد يعتمد على عدة مصادر للـ state. في Options API، تضطر إلى تقسيم هذا المنطق بين عدة خيارات (methods، computed، watch)، مما يجعل تتبع تدفق البيانات صعباً. على سبيل المثال، إذا كان لديك computed property يعتمد على قيمة في الـ data ويتفاعل مع event من الـ template، ستجد نفسك تقفز بين عدة أقسام في الملف الواحد، وهذا ما يجعل الكود صعب الفهم والصيانة على المدى الطويل.
// مثال على Options API مع منطق متفرق
>
export default {
data() {
return {
count: 0,
price: 10
}
},
computed: {
total() {
return this.count * this.price;
}
},
methods: {
increment() {
this.count++;
},
updatePrice(newPrice) {
this.price = newPrice;
}
},
watch: {
total(newVal) {
console.log(`Total updated: ${newVal}`);
}
}
}
</script>
<template>
<div>
<button @click="increment">Increment</button>
<input v-model.number="price" type="number">
<p>Total: {{ total }}</p>
</div>
</template>في المقابل، Composition API يقدم طريقة مختلفة تماماً لتنظيم الكود. بدلاً من تقسيم المنطق إلى خيارات، يمكنك تجميع كل ما يتعلق بميزة معينة في مكان واحد باستخدام الـ setup function أو الـ ` setup>` syntax. هذا يعني أنك تستطيع كتابة كل الـ logic المتعلق بالـ state، الـ side effects، والـ computed values في مكان واحد، مما يجعل الكود أكثر قابلية للقراءة والصيانة. ولكن الأهم من ذلك هو كيف يتعامل Composition API مع الـ reactivity خلف الكواليس.
في Composition API، تستخدم دوال مثل ref وreactive لإنشاء كائنات reactive. عندما تقوم بإنشاء ref، تقوم Vue بإنشاء كائن يحتوي على خاصية value، وتقوم بمراقبة هذه الخاصية باستخدام الـ Proxy API. هذا يعني أن أي تغيير على value سيؤدي إلى تحديث الـ DOM والعناصر المعتمدة عليه. الفرق هنا هو أن Composition API يسمح لك بتجميع كل الـ logic المتعلق بميزة معينة في مكان واحد، مما يقلل من الحاجة للقفز بين أقسام مختلفة في الملف. ولكن هذا لا يعني أن Composition API هو الحل السحري لكل المشاكل — فهناك حالات يكون فيها Options API أكثر مناسبة، وسنتحدث عن ذلك لاحقاً.
// نفس المثال باستخدام Composition API
setup>
import { ref, computed, watch } from 'vue';
const count = ref(0);
const price = ref(10);
const total = computed(() => count.value * price.value);
const increment = () => {
count.value++;
};
const updatePrice = (newPrice) => {
price.value = newPrice;
};
watch(total, (newVal) => {
console.log(`Total updated: ${newVal}`);
});
</script>
<template>
<div>
<button @click="increment">Increment</button>
<input v-model.number="price" type="number">
<p>Total: {{ total }}</p>
</div>
</template>أحد الأسئلة الشائعة عند المقارنة بين Composition API وOptions API هو: هل يؤثر اختيارك على أداء التطبيق؟ الإجابة القصيرة هي: نعم، ولكن ليس بالطريقة التي تتوقعها. في معظم الحالات، الفرق في الأداء بين الاثنين يكون ضئيلاً جداً، لأن Vue تستخدم نفس نظام الـ reactivity في كلا الحالتين. ولكن هناك سيناريوهات معينة يمكن أن يؤدي فيها استخدام أحد الـ APIs إلى مشاكل في الأداء أو استهلاك الذاكرة.
في Options API، كل مكون يكون له instance خاص به يحتوي على جميع الـ data، الـ methods، والـ computed properties. هذا يعني أن حتى إذا لم تستخدم بعض الـ properties في الـ template، فإنها ستظل موجودة في الذاكرة كجزء من الـ instance. في التطبيقات الكبيرة التي تحتوي على مئات المكونات، يمكن أن يؤدي هذا إلى استهلاك غير ضروري للذاكرة، خاصة إذا كانت بعض المكونات تحتوي على بيانات كبيرة أو معقدة. على سبيل المثال، إذا كان لديك مكون يعرض جدول بيانات يحتوي على آلاف الصفوف، وقد يحتوي على computed properties معقدة، فإن استخدام Options API قد يؤدي إلى استهلاك ذاكرة أعلى مما هو مطلوب.
من ناحية أخرى، Composition API يسمح لك بتقسيم الـ logic إلى دوال صغيرة يمكن إعادة استخدامها، وهذا يمكن أن يقلل من استهلاك الذاكرة في بعض الحالات. ولكن الأهم هو أن Composition API يسمح لك بتحديد بالضبط ما هو reactive وما هو ليس كذلك. على سبيل المثال، يمكنك استخدام ref فقط للقيم التي تحتاج إلى reactivity، واستخدام متغيرات عادية للقيم الثابتة. هذا يمكن أن يقلل من الحمل على نظام الـ reactivity، ويحسن الأداء في التطبيقات الكبيرة والمعقدة.
// مثال على تحسين الذاكرة باستخدام Composition API
setup>
import { ref, computed } from 'vue';
// قيمة ثابتة لا تحتاج إلى reactivity
const MAX_ITEMS = 100;
// قيمة تحتاج إلى reactivity
const items = ref([]);
// computed property معقدة ولكن يتم حسابها فقط عند الحاجة
const filteredItems = computed(() => {
return items.value.filter(item => item.price > 50);
});
// دالة لا تحتاج إلى أن تكون reactive
function loadItems() {
// محاكاة تحميل بيانات من API
items.value = Array.from({ length: MAX_ITEMS }, (_, i) => ({
id: i,
price: Math.random() * 100
}));
}
</script>ولكن هناك جانب آخر يجب أخذه في الاعتبار: الـ Event Loop. في Options API، كل الـ methods والـ computed properties يتم تعريفها كجزء من الـ instance، وهذا يعني أن Vue تقوم بإنشاءclosures لكل منها. في التطبيقات الكبيرة، يمكن أن يؤدي هذا إلى زيادة في استهلاك الذاكرة بسبب كثرة الـ closures. في المقابل، Composition API يسمح لك بتعريف الدوال خارج الـ setup function إذا كانت لا تعتمد على الـ reactive state، مما يقلل من عدد الـ closures المطلوبة. هذا قد يبدو تفصيلاً صغيراً، ولكن في التطبيقات الكبيرة التي تحتوي على آلاف المكونات، يمكن أن يكون له تأثير ملحوظ على الأداء واستهلاك الذاكرة.
في بداية المشروع، قد يبدو أن استخدام Options API أسهل وأكثر تنظيماً، خاصة للمطورين الذين اعتادوا على هذا النمط. ولكن مع نمو المشروع وتزايد تعقيده، تبدأ المشاكل في الظهور. أحد أكبر التحديات مع Options API هو أنه يجبرك على تقسيم الـ logic إلى أقسام مختلفة، مما يجعل تتبع تدفق البيانات صعباً. على سبيل المثال، إذا كان لديك مكون يحتوي على عدة computed properties تعتمد على بعضها البعض، وتستخدم هذه الـ computed properties في عدة methods، ستجد نفسك تقفز باستمرار بين أقسام مختلفة في الملف، وهذا ما يجعل الكود صعب الفهم والصيانة.
في أحد المشاريع التي عملت عليها، كان لدينا مكون لإدارة عربة التسوق في منصة e-commerce. كان الملف مكوناً من أكثر من ١٠٠٠ سطر، وكان يحتوي على عشرات الـ computed properties والـ methods التي تتفاعل مع بعضها البعض. المشكلة لم تكن في حجم الملف فقط، بل في صعوبة تتبع كيف تتفاعل هذه الـ properties مع بعضها البعض. عندما حاولنا إضافة ميزة جديدة، استغرق الأمر منا أياماً لفهم كيف يؤثر التغيير على باقي الـ logic. في النهاية، قررنا إعادة كتابة المكون باستخدام Composition API، وقمنا بتقسيم الـ logic إلى دوال صغيرة يمكن إعادة استخدامها، مما جعل الكود أكثر قابلية للصيانة والتوسع.
// مثال على تقسيم منطق معقد باستخدام Composition API
setup>
import { ref, computed } from 'vue';
// منطق إدارة الكمية
const quantity = ref(1);
const incrementQuantity = () => quantity.value++;
const decrementQuantity = () => {
if (quantity.value > 1) quantity.value--;
};
// منطق إدارة السعر
const price = ref(99.99);
const discount = ref(0);
const finalPrice = computed(() => {
return price.value * (1 - discount.value / 100);
});
// منطق إدارة الخصم
const applyDiscount = (percentage) => {
discount.value = percentage;
};
// منطق إجمالي السعر
const totalPrice = computed(() => {
return finalPrice.value * quantity.value;
});
// منطق التحقق من المخزون
const stock = ref(10);
const isOutOfStock = computed(() => stock.value <= 0);
const checkStock = () => {
if (quantity.value > stock.value) {
alert('الكمية المطلوبة غير متوفرة في المخزون');
quantity.value = stock.value;
}
};
// دالة واحدة لإدارة كل شيء عند إضافة المنتج للعربة
const addToCart = () => {
checkStock();
if (!isOutOfStock.value) {
console.log(`Added ${quantity.value} items to cart. Total: $${totalPrice.value.toFixed(2)}`);
stock.value -= quantity.value;
}
};
</script>
<template>
<div>
<button @click="decrementQuantity" :disabled="quantity <= 1">-</button>
<span>{{ quantity }}</span>
<button @click="incrementQuantity">+</button>
<button @click="applyDiscount(10)">Apply 10% Discount</button>
<p>Price: ${{ finalPrice.toFixed(2) }}</p>
<p>Total: ${{ totalPrice.toFixed(2) }}</p>
<button @click="addToCart" :disabled="isOutOfStock">Add to Cart</button>
</div>
</template>في المقابل، Composition API يسمح لك بتجميع كل الـ logic المتعلق بميزة معينة في مكان واحد، مما يجعل الكود أكثر قابلية للقراءة والصيانة. ولكن هذا لا يعني أن Composition API هو الحل الأمثل لكل الحالات. في المشاريع الصغيرة أو المكونات البسيطة، قد يكون Options API أكثر مناسبة، لأنه يوفر هيكلاً واضحاً ومنظماً. ولكن عندما يبدأ المشروع في النمو، ويصبح الـ logic أكثر تعقيداً، يصبح Composition API الخيار الأفضل، لأنه يسمح لك بتقسيم الـ logic إلى وحدات صغيرة يمكن إعادة استخدامها وإدارتها بسهولة.
إذا كنت تعمل في فريق يستخدم TypeScript، فإن اختيارك بين Composition API وOptions API يمكن أن يكون له تأثير كبير على تجربة التطوير. أحد أكبر مزايا Composition API هو أنه مصمم للعمل بشكل أفضل مع TypeScript. في Options API، تواجه تحديات عند محاولة كتابة أنواع معقدة للـ data أو الـ computed properties، لأنك مضطر للتعامل مع الـ this الذي لا يمكن تحديد نوعه بسهولة. في المقابل، Composition API يسمح لك بتحديد أنواع واضحة لكل متغير ودالة، مما يجعل الكود أكثر أماناً وسهولة في الصيانة.
// مثال على استخدام TypeScript مع Composition API
setup lang="ts">
import { ref, computed } from 'vue';
interface Product {
id: number;
name: string;
price: number;
stock: number;
}
const product = ref<Product>({
id: 1,
name: 'Vue 3 Book',
price: 29.99,
stock: 10
});
const quantity = ref<number>(1);
const totalPrice = computed<number>(() => {
return product.value.price * quantity.value;
});
const incrementQuantity = (): void => {
if (quantity.value < product.value.stock) {
quantity.value++;
}
};
const addToCart = (): void => {
if (quantity.value > 0 && quantity.value <= product.value.stock) {
console.log(`Added ${quantity.value} of ${product.value.name} to cart`);
product.value.stock -= quantity.value;
}
};
</script>بالإضافة إلى ذلك، Composition API يتكامل بشكل أفضل مع أدوات التطوير الحديثة مثل Vite وVue DevTools. على سبيل المثال، في Vue DevTools، يمكنك رؤية كل الـ reactive state والـ computed properties بشكل منفصل وواضح عند استخدام Composition API، مما يجعل عملية تصحيح الأخطاء أسهل بكثير. في المقابل، في Options API، قد تجد نفسك تبحث عن خاصية معينة بين عشرات الـ properties في الـ instance، وهذا يمكن أن يكون محبطاً في المشاريع الكبيرة.
من تجربتي الشخصية، عندما انتقلت من Options API إلى Composition API في مشروع يستخدم TypeScript، لاحظت تحسناً كبيراً في جودة الكود وتقليل الأخطاء المتعلقة بالأنواع. كما أن تكامل Composition API مع أدوات التطوير جعل عملية تصحيح الأخطاء أسرع وأكثر كفاءة. ولكن هذا لا يعني أن Options API لا يدعم TypeScript — بل يمكنك استخدامه، ولكن ستجد نفسك تكافح مع أنواع الـ this ومعقدة بعض الشيء.
حتى مع فهم الفروقات بين Composition API وOptions API، هناك بعض الفخاخ الشائعة التي يقع فيها المطورون عند استخدام كل منهما. في Options API، أحد أكبر الأخطاء هو الاعتماد المفرط على الـ watch بدلاً من الـ computed properties. الـ watch مفيدة عندما تريد تنفيذ side effect عند تغيير قيمة معينة، ولكن استخدامها بدلاً من الـ computed properties يمكن أن يؤدي إلى كود غير فعال وصعب الفهم. على سبيل المثال، إذا كنت تستخدم watch لتحديث قيمة تعتمد على عدة مصادر للـ state، فقد تجد نفسك تكتب كوداً معقداً وغير ضروري.
// مثال على استخدام watch بدلاً من computed property (فخ شائع)
>
export default {
data() {
return {
firstName: '',
lastName: '',
fullName: ''
}
},
watch: {
firstName() {
this.fullName = `${this.firstName} ${this.lastName}`;
},
lastName() {
this.fullName = `${this.firstName} ${this.lastName}`;
}
}
}
</script>
<!-- الحل الأفضل باستخدام computed property -->
<script>
export default {
data() {
return {
firstName: '',
lastName: ''
}
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
}
</script>في Composition API، أحد الفخاخ الشائعة هو نسيان استخدام .value عند الوصول إلى قيم الـ ref داخل الـ setup function. لأن الـ ref تُرجع كائناً يحتوي على خاصية value، فإن نسيان استخدام .value يمكن أن يؤدي إلى أخطاء صعبة التصحيح. على سبيل المثال، إذا كتبت `count + 1` بدلاً من `count.value + 1`، ستحصل على خطأ لأن count هو كائن وليس قيمة عددية. هذا الخطأ شائع جداً بين المطورين الذين ينتقلون من Options API إلى Composition API، لأنهم اعتادوا على الوصول المباشر للقيم دون الحاجة إلى استخدام .value.
// فخ نسيان .value في Composition API
setup>
import { ref } from 'vue';
const count = ref(0);
// ❌ خطأ: count هو كائن، وليس قيمة عددية
const increment = () => {
count = count + 1; // TypeError: Assignment to constant variable.
};
// ✅ صحيح: استخدام .value
const correctIncrement = () => {
count.value++;
};
</script>فخ آخر شائع في Composition API هو عدم فهم الفرق بين ref وreactive. الـ ref تُستخدم للقيم الأولية مثل الأرقام والنصوص، بينما reactive تُستخدم للكائنات والمصفوفات. استخدام reactive للقيم الأولية يمكن أن يؤدي إلى سلوك غير متوقع، لأن reactive تُرجع proxy للكائن، وليس قيمة مباشرة. على سبيل المثال، إذا استخدمت reactive لقيمة عددية، وحاولت تحديثها، قد لا يعمل الـ reactivity كما تتوقع.
// فخ استخدام reactive للقيم الأولية
setup>
import { reactive } from 'vue';
// ❌ خطأ: reactive تُستخدم للكائنات، وليس للأرقام
const count = reactive(0);
const increment = () => {
count++; // لن يعمل كما تتوقع
};
// ✅ صحيح: استخدام ref للأرقام
import { ref } from 'vue';
const correctCount = ref(0);
const correctIncrement = () => {
correctCount.value++;
};
</script>بعد كل هذه المقارنة، قد تتساءل: أيهما يجب أن تختار؟ الحقيقة هي أنه لا يوجد فائز مطلق — كل API له حالات استخدامه المناسبة. إذا كنت تعمل على مشروع صغير أو مكونات بسيطة، فقد يكون Options API أكثر مناسبة، لأنه يوفر هيكلاً واضحاً ومنظماً. ولكن إذا كنت تعمل على مشروع كبير ومعقد، أو إذا كنت تستخدم TypeScript، فإن Composition API هو الخيار الأفضل، لأنه يسمح لك بتقسيم الـ logic إلى وحدات صغيرة يمكن إعادة استخدامها، ويوفر تكاملاً أفضل مع أدوات التطوير الحديثة.
في رأيي الشخصي، Composition API هو مستقبل Vue، وليس فقط لأنه أكثر مرونة، بل لأنه يحل المشاكل الحقيقية التي يواجهها المطورون في المشاريع الكبيرة. ولكن هذا لا يعني أن Options API سيختفي — بل سيظل خياراً جيداً للمشاريع الصغيرة والمكونات البسيطة. إذا كنت تخطط لمشروع جديد، أو تفكر في إعادة هيكلة مشروع قائم، فإن استخدام Composition API سيكون استثماراً جيداً على المدى الطويل. ولكن تذكر أن المفتاح ليس في اختيار الـ API فقط، بل في كيفية تنظيم الكود الخاص بك وتجنب الفخاخ الشائعة التي ذكرناها سابقاً.
الخطوة التالية؟ جرب كلا الـ APIs في مشروع صغير، وانظر بنفسك كيف يؤثر كل منهما على قابلية الصيانة والأداء. لا تعتمد فقط على آراء الآخرين — جرب بنفسك، وسترى الفرق بوضوح. وإذا كنت تعمل في فريق، ناقش مع زملائك أي الـ APIs يناسب احتياجات المشروع بشكل أفضل. في النهاية، الهدف هو كتابة كود نظيف، فعال، وسهل الصيانة — بغض النظر عن الـ API الذي تختاره.
البرمجة ليست حول كتابة الكود الذي يفهمه الحاسوب فقط، بل عن كتابة الكود الذي يفهمه المطورون الآخرون بعد ستة أشهر.
— مجهول