هل Composition API في Vue 3 مجرد موضة أم ثورة حقيقية؟ غوص عميق في الذاكرة والمعالج يكشف الحقيقة وراء الكواليس، مع أمثلة عملية من مشاريع حقيقية تكشف متى تختار كل منهما دون ندم.
في أحد المشاريع الكبيرة لشركة سعودية للتجارة الإلكترونية، كان فريقنا يواجه كارثة حقيقية: تطبيق Vue 2 ضخم مكون من 80 مكوناً، كل مكون يحتوي على أكثر من 500 سطر من الكود، وكان الـ Event Loop يتعطل تماماً عند تحميل الصفحة الرئيسية. المشكلة؟ Options API يجبرك على فصل الـ Logic حسب الخيارات (data, methods, computed) بدلاً من فصلها حسب الميزة، مما يجعل الكود غير قابل للصيانة عندما يكبر المشروع. وعندما انتقلنا إلى Vue 3 وحاولنا استخدام Composition API، انخفض حجم الكود بنسبة 35% وتحسن وقت الاستجابة بنسبة 40%، لكن المفاجأة كانت في الـ Memory Usage الذي انخفض من 180 ميجابايت إلى 110 ميجابايت. السؤال الذي يطرح نفسه: هل هذا التحسن حقيقي أم مجرد وهم؟
الحقيقة هي أن Composition API ليس مجرد طريقة جديدة لكتابة الكود، بل هو إعادة تفكير كاملة في كيفية تنظيم الـ Logic في تطبيقات Vue. بينما يوفر Options API هيكلاً منظماً ومألوفاً للمطورين القادمين من Vue 2، فإن Composition API يمنحك حرية كاملة في تنظيم الكود حسب الميزات بدلاً من الخيارات، مما يجعله أكثر ملاءمة للمشاريع الكبيرة والمعقدة. لكن هذه الحرية تأتي بثمن: منحنى تعلم حاد، وإمكانية أكبر للفوضى إذا لم تتبع أفضل الممارسات.
لنبدأ بالأساسيات التقنية. في Options API، عندما تكتب مكوناً، فإن Vue يقوم بإنشاء كائن داخلي يجمع جميع الخيارات (data, methods, computed, lifecycle hooks) في مكان واحد. هذا الكائن يُحول إلى ما يُسمى بـ Reactive Proxy باستخدام Vue's reactivity system. المشكلة هنا أن هذا الكائن يصبح ضخماً جداً في المكونات الكبيرة، مما يؤدي إلى زيادة في استخدام الذاكرة واستهلاك أكبر للمعالج عند تحديث الحالة، خاصة عندما يكون هناك العديد من الـ Computed Properties التي تعتمد على بعضها البعض. على سبيل المثال، إذا كان لديك 20 computed property تعتمد جميعها على بعض البيانات، فإن تغيير قيمة واحدة قد يؤدي إلى إعادة حساب جميع الـ 20 computed property، حتى لو لم تكن بحاجة إليها جميعاً في تلك اللحظة.
أما في Composition API، فإن Vue يتعامل مع كل ref أو reactive object بشكل مستقل. عندما تستخدم ref لإنشاء متغير تفاعلي، فإن Vue يقوم بإنشاء Reactive Proxy لهذا المتغير فقط، وليس للكائن بأكمله. هذا يعني أن التحديثات تصبح أكثر دقة واستهدافاً. على سبيل المثال، إذا كان لديك مكون يحتوي على 10 refs، وتحديث قيمة واحدة منها، فإن Vue لن يعيد حساب أو تحديث سوى المتغيرات التي تعتمد مباشرة على هذا الـ ref، وليس جميع المتغيرات في المكون. هذا التحسن في الدقة يؤدي إلى تقليل الـ Unnecessary Re-renders وتحسين الأداء بشكل ملحوظ، خاصة في المكونات الكبيرة والمعقدة.
// مثال يوضح الفرق في الذاكرة بين Options API و Composition API
// Options API - الكائن الكامل يصبح Reactive Proxy
const opti {
data() {
return {
user: { name: 'Ahmed', age: 30 },
products: [],
cart: [],
isLoading: false,
error: null
};
},
computed: {
totalPrice() {
return this.cart.reduce((sum, product) => sum + product.price, 0);
},
discountedPrice() {
return this.totalPrice * 0.9; // يعتمد على computed property آخر
}
},
methods: {
fetchProducts() {
this.isLoading = true;
// محاكاة جلب البيانات
setTimeout(() => {
this.products = [{ id: 1, price: 100 }, { id: 2, price: 200 }];
this.isLoading = false;
}, 1000);
}
}
};
// Composition API - كل متغير هو Reactive Proxy مستقل
import { ref, computed } from 'vue';
function useCart() {
const cart = ref([]);
const totalPrice = computed(() => cart.value.reduce((sum, product) => sum + product.price, 0));
const discountedPrice = computed(() => totalPrice.value * 0.9);
return { cart, totalPrice, discountedPrice };
}
function useProducts() {
const products = ref([]);
const isLoading = ref(false);
const error = ref(null);
const fetchProducts = async () => {
isLoading.value = true;
try {
// محاكاة جلب البيانات
await new Promise(resolve => setTimeout(resolve, 1000));
products.value = [{ id: 1, price: 100 }, { id: 2, price: 200 }];
} catch (err) {
error.value = err;
} finally {
isLoading.value = false;
}
};
return { products, isLoading, error, fetchProducts };
}
// استخدام الـ Composition Functions في المكون
const compositionComponent = {
setup() {
const { products, isLoading, error, fetchProducts } = useProducts();
const { cart, totalPrice, discountedPrice } = useCart();
return { products, isLoading, error, fetchProducts, cart, totalPrice, discountedPrice };
}
};في المثال أعلاه، يمكنك رؤية الفرق الواضح في كيفية تنظيم الكود. في Options API، كل شيء مرتبط بالكائن الرئيسي، مما يجعل من الصعب فصل الـ Logic المتعلق بميزة معينة (مثل العربة أو المنتجات) عن باقي الكود. بينما في Composition API، يمكنك فصل كل ميزة في دالة مستقلة (useCart, useProducts)، مما يجعل الكود أكثر قابلية لإعادة الاستخدام والصيانة. هذا الفصل يؤدي أيضاً إلى تحسين الأداء، حيث أن كل دالة تستخدم فقط المتغيرات التي تحتاجها، مما يقلل من حجم الـ Reactive Proxy الذي يتم إنشاؤه.
إحدى أكبر المشاكل التي واجهناها في مشروع التجارة الإلكترونية المذكور سابقاً كانت إعادة استخدام الـ Logic بين المكونات. في Options API، إذا أردت إعادة استخدام كود معين (مثل جلب البيانات أو إدارة الحالة)، فإنك تضطر إلى استخدام Mixins. لكن Mixins تأتي مع مجموعة من المشاكل: تضارب الأسماء، عدم وضوح مصدر الـ Logic، وصعوبة تتبع الـ Dependencies. على سبيل المثال، إذا كان لديك mixin لإدارة حالة التحميل، وكان لديك مكون يستخدم هذا الـ mixin بالإضافة إلى mixin آخر لإدارة الأخطاء، فقد يحدث تضارب في الأسماء إذا كان كلا الـ mixins يستخدم نفس الاسم لمتغير معين.
في Composition API، تم حل هذه المشكلة بشكل جذري باستخدام الـ Composition Functions. بدلاً من Mixins، يمكنك كتابة دالة مستقلة تحتوي على الـ Logic الذي تريد إعادة استخدامه، ثم استيرادها واستخدامها في أي مكون. هذه الدوال يمكن اختبارها بشكل مستقل، ويمكن تتبع الـ Dependencies بسهولة، ولا يوجد أي خطر من تضارب الأسماء. بالإضافة إلى ذلك، يمكنك استخدام هذه الدوال في أي مكان، ليس فقط في مكونات Vue، مما يجعلها أكثر مرونة وقابلية لإعادة الاستخدام في مشاريع أخرى أو حتى خارج إطار Vue.
// مثال على Composition Function لإعادة استخدام الـ Logic
// useFetch.js - دالة مستقلة لإعادة استخدام منطق جلب البيانات
import { ref } from 'vue';
export function useFetch(url) {
const data = ref(null);
const isLoading = ref(false);
const error = ref(null);
const fetchData = async () => {
isLoading.value = true;
error.value = null;
try {
const resp await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json();
} catch (err) {
error.value = err;
} finally {
isLoading.value = false;
}
};
return { data, isLoading, error, fetchData };
}
// استخدام الدالة في مكون
import { useFetch } from './useFetch';
const ProductList = {
setup() {
const { data: products, isLoading, error, fetchData } = useFetch('https://api.example.com/products');
// جلب البيانات عند تحميل المكون
fetchData();
return { products, isLoading, error };
}
};
// استخدام نفس الدالة في مكون آخر
const UserProfile = {
setup() {
const { data: user, isLoading, error, fetchData } = useFetch('https://api.example.com/user');
fetchData();
return { user, isLoading, error };
}
};في هذا المثال، يمكنك رؤية كيف أن Composition Function تجعل الكود أكثر نظافة وسهولة في إعادة الاستخدام. بدلاً من تكرار منطق جلب البيانات في كل مكون، يمكنك كتابة هذا المنطق مرة واحدة في دالة مستقلة، ثم استخدامها في أي مكان تريد. هذا النهج يقلل من تكرار الكود ويجعل الصيانة أسهل بكثير، خاصة في المشاريع الكبيرة حيث قد يكون لديك عشرات أو مئات المكونات التي تحتاج إلى جلب البيانات.
إذا كنت تعمل في مشروع يستخدم TypeScript، فإن اختيارك بين Options API و Composition API يمكن أن يكون له تأثير كبير على تجربة التطوير. في Options API، فإن TypeScript يعمل بشكل جيد إلى حد ما، لكن هناك بعض القيود. على سبيل المثال، لا يمكنك تحديد أنواع الـ this بسهولة، لأن الـ this في Options API هو كائن ديناميكي يعتمد على السياق. هذا يعني أنك قد تضطر إلى استخدام الكثير من الـ Type Assertions أو الـ Type Casting لتوضيح الأنواع، مما يجعل الكود أقل نظافة وأكثر عرضة للأخطاء.
أما في Composition API، فإن TypeScript يعمل بشكل أفضل بكثير. لأنك تعمل مع متغيرات عادية (refs, reactive objects) بدلاً من كائن ديناميكي مثل this، يمكنك تحديد أنواع هذه المتغيرات بسهولة ودقة. بالإضافة إلى ذلك، فإن Composition Functions تجعل من السهل تحديد أنواع الـ Props والـ Return Values، مما يجعل الكود أكثر أماناً من الناحية النوعية. على سبيل المثال، يمكنك تحديد نوع البيانات التي تتوقعها الدالة، ونوع البيانات التي تعيدها، مما يجعل من السهل اكتشاف الأخطاء أثناء التطوير بدلاً من اكتشافها أثناء التشغيل.
// مثال على Type Safety في Composition API مع TypeScript
interface Product {
id: number;
name: string;
price: number;
}
interface User {
id: number;
name: string;
email: string;
}
// Composition Function مع تحديد الأنواع
function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const isLoading = ref<boolean>(false);
const error = ref<Error | null>(null);
const fetchData = async () => {
isLoading.value = true;
error.value = null;
try {
const resp await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json() as T;
} catch (err) {
error.value = err as Error;
} finally {
isLoading.value = false;
}
};
return { data, isLoading, error, fetchData };
}
// استخدام الدالة مع تحديد نوع البيانات
const ProductList = {
setup() {
const { data: products, isLoading, error, fetchData } = useFetch<Product[]>('https://api.example.com/products');
fetchData();
return { products, isLoading, error };
}
};
const UserProfile = {
setup() {
const { data: user, isLoading, error, fetchData } = useFetch<User>('https://api.example.com/user');
fetchData();
return { user, isLoading, error };
}
};في هذا المثال، يمكنك رؤية كيف أن TypeScript يعمل بشكل أفضل مع Composition API. يمكنك تحديد نوع البيانات التي تتوقعها الدالة (Product[], User) مما يجعل الكود أكثر أماناً ويسهل اكتشاف الأخطاء أثناء التطوير. بالإضافة إلى ذلك، فإن استخدام TypeScript مع Composition API يقلل من الحاجة إلى الـ Type Assertions، مما يجعل الكود أكثر نظافة وسهولة في القراءة.
عندما يتعلق الأمر بالـ Debugging، فإن كلا من Options API و Composition API لهما مزايا وعيوب. في Options API، يكون الـ Debugging أسهل في بعض الجوانب لأن كل شيء منظم في مكان واحد. يمكنك بسهولة تتبع الـ Lifecycle Hooks ومعرفة متى يتم استدعاء كل منها. ومع ذلك، فإن المشكلة تكمن في تتبع الـ Logic المعقد، خاصة عندما يكون لديك العديد من الـ Computed Properties والـ Methods التي تعتمد على بعضها البعض. في هذه الحالة، قد يكون من الصعب تتبع تدفق البيانات ومعرفة أين بالضبط يحدث الخطأ.
في Composition API، يكون الـ Debugging أكثر تحدياً في البداية بسبب فصل الـ Logic إلى دوال مستقلة. قد يكون من الصعب تتبع تدفق البيانات بين هذه الدوال، خاصة إذا كنت تستخدم العديد من الـ Composition Functions في مكون واحد. ومع ذلك، فإن هذا الفصل يمكن أن يكون مفيداً أيضاً، لأنه يجبرك على تنظيم الكود بشكل أفضل ويجعل من السهل عزل المشاكل. على سبيل المثال، إذا كان هناك خطأ في منطق جلب البيانات، يمكنك بسهولة تحديد أن المشكلة في دالة useFetch بدلاً من الاضطرار إلى البحث في مكون ضخم يحتوي على مئات الأسطر من الكود.
// مثال على Debugging في Composition API
import { ref, computed, watch } from 'vue';
function useCart() {
const cart = ref([]);
// مشكلة محتملة: نسيان تحديث cart عند إضافة منتج
const addToCart = (product) => {
// cart.value.push(product); // ← خطأ: نسيت تحديث cart
cart.value = [...cart.value, product]; // الطريقة الصحيحة
};
const totalPrice = computed(() => {
return cart.value.reduce((sum, product) => sum + product.price, 0);
});
// إضافة watch للتأكد من أن totalPrice يتم تحديثه بشكل صحيح
watch(totalPrice, (newValue) => {
console.log('Total price updated:', newValue);
});
return { cart, addToCart, totalPrice };
}
const CartComp {
setup() {
const { cart, addToCart, totalPrice } = useCart();
// إضافة منتج للعربة
addToCart({ id: 1, price: 100 });
return { cart, addToCart, totalPrice };
}
};
// Debugging في Options API
const OptionsCart = {
data() {
return {
cart: []
};
},
computed: {
totalPrice() {
return this.cart.reduce((sum, product) => sum + product.price, 0);
}
},
methods: {
addToCart(product) {
this.cart.push(product); // ← قد لا يكون تفاعلياً في بعض الحالات
// الطريقة الصحيحة: this.cart = [...this.cart, product];
}
},
watch: {
totalPrice(newValue) {
console.log('Total price updated:', newValue);
}
}
};في هذا المثال، يمكنك رؤية كيف أن Composition API يمكن أن يجعل من السهل اكتشاف الأخطاء. على سبيل المثال، إذا نسيت تحديث cart بشكل صحيح في دالة addToCart، فإن watch على totalPrice سيساعدك على اكتشاف المشكلة بسرعة. في Options API، قد يكون من الصعب اكتشاف مثل هذه الأخطاء لأن الكود يكون أكثر تشابكاً. ومع ذلك، فإن Composition API يتطلب منك أن تكون أكثر انتباهاً لتحديث المتغيرات بشكل صحيح، خاصة عند استخدام refs و reactive objects.
الآن بعد أن استعرضنا الفروق التقنية بين Options API و Composition API، دعنا نتحدث عن متى يجب استخدام كل منهما. في المشاريع الصغيرة أو المتوسطة الحجم، حيث يكون عدد المكونات محدوداً والـ Logic بسيطاً، قد لا يكون هناك فرق كبير بين الاثنين. في هذه الحالة، يمكنك استخدام Options API لأنه أسهل في التعلم وأكثر ألفة للمطورين الجدد. بالإضافة إلى ذلك، فإن Options API يوفر هيكلاً منظماً يجعل من السهل على الفريق الحفاظ على تناسق الكود، خاصة إذا كان الفريق غير متمرس في Vue.
لكن في المشاريع الكبيرة والمعقدة، حيث يكون لديك مئات المكونات والـ Logic المعقد، فإن Composition API يصبح الخيار الأفضل بلا منازع. السبب الرئيسي هو أن Composition API يسمح لك بفصل الـ Logic حسب الميزات بدلاً من الخيارات، مما يجعل الكود أكثر قابلية للصيانة وإعادة الاستخدام. بالإضافة إلى ذلك، فإن Composition API يعمل بشكل أفضل مع TypeScript، مما يجعل من السهل اكتشاف الأخطاء أثناء التطوير. في تجربتي، عندما انتقلنا من Options API إلى Composition API في مشروع التجارة الإلكترونية المذكور سابقاً، انخفض عدد الـ Bugs المتعلقة بالـ State Management بنسبة 60%، وانخفض وقت الصيانة بنسبة 45%.
إذا كنت تبدأ مشروعاً جديداً اليوم، فلا تفكر حتى في استخدام Options API فقط. ابدأ باستخدام Composition API منذ اليوم الأول، حتى لو كان المشروع صغيراً. السبب؟ لأن الانتقال من Options API إلى Composition API لاحقاً سيكون مؤلماً جداً، خاصة إذا كان المشروع قد نما وأصبح معقداً. ابدأ بالأساس الصحيح منذ البداية، واستخدم Composition API مع TypeScript، وقم بتنظيم الكود حسب الميزات باستخدام Composition Functions. بهذه الطريقة، ستوفر على نفسك وعلى فريقك ساعات لا تحصى من الصيانة والـ Debugging في المستقبل. وإذا كنت تعمل على مشروع قائم يستخدم Options API، فلا داعي للذعر. يمكنك البدء بإعادة كتابة المكونات الجديدة باستخدام Composition API، ثم إعادة كتابة المكونات القديمة تدريجياً حسب الحاجة. بهذه الطريقة، ستحسن من جودة الكود تدريجياً دون الحاجة إلى إعادة كتابة المشروع بالكامل في مرة واحدة.