هل Composition API مجرد موضة أم ثورة حقيقية؟ اكتشف الفرق العميق بين الواجهتين في Vue 3 من منظور الذاكرة والأداء، مع أمثلة عملية تكشف متى تختار كل منهما في مشاريع الإنتاج الحقيقية.
عندما رأيت لأول مرة مشروعاً يستخدم Vue 3 Composition API، شعرت وكأنني فتحت صندوق باندورا. الكود كان أنيقاً ومرتباً، لكن عقلي بدأ يحسب التكلفة الخفية: كم عدد الـ Closures الجديدة التي ستنشئها هذه الـ refs؟ كيف سيتصرف الـ Event Loop مع كل هذه الـ reactive effects؟ وهل هذا التحول من Options API يستحق حقاً التخلي عن البنية المألوفة التي اعتدناها؟ الحقيقة هي أن الاختيار بين الواجهتين ليس مجرد مسألة ذوق، بل قرار هندسي يؤثر على أداء التطبيق، قابلية الصيانة، وحتى راتبك كمطور. دعونا ننزع القفازات ونرى ما يحدث خلف الكواليس.
في أحد مشاريع الإنتاج التي عملت عليها العام الماضي، اضطررنا لإعادة كتابة مكون ضخم من Options API إلى Composition API بعد أن بدأ السيرفر في التعليق عند تحميل 500 سجل بيانات. المشكلة لم تكن في الـ API نفسه، بل في الطريقة التي كان الـ Options API ينشئ بها كائنات الـ watchers لكل خاصية في الـ data. عندما انتقلنا إلى Composition API، قلصنا حجم الذاكرة المستهلكة بنسبة 40% ببساطة عن طريق تجميع الـ reactive properties ذات الصلة في بلوكات منطقية. لكن هذا ليس دائماً الحل السحري - في بعض الحالات، قد يؤدي Composition API إلى تعقيد غير مبرر، خاصة في المكونات البسيطة التي لا تحتاج إلى إعادة استخدام المنطق.
الفرق الأساسي بين الواجهتين ليس مجرد مكان كتابة الكود، بل كيف يتعامل Vue مع المكون داخلياً. في Options API، كل مكون هو كائن جافاسكريبت عادي له خصائص محددة مثل data، methods، computed، وغيرها. عندما تقوم بتعريف مكون بهذه الطريقة، يقوم Vue بإنشاء ما يسمى بـ "reactivity proxy" حول الكائن بأكمله، مما يعني أن كل خاصية في الـ data تصبح reactive تلقائياً، سواء كنت تحتاج ذلك أم لا. هذا النهج مريح لكنه مكلف: تخيل مكوناً بسيطاً لعرض قائمة مهام يحتوي على 20 خاصية، بينما تستخدم فعلياً 3 منها فقط في الـ template. الـ 17 خاصية المتبقية لا تزال تستهلك موارد الـ reactivity دون داعٍ.
في المقابل، Composition API يسمح لك بتحديد بالضبط ما تريد جعله reactive باستخدام دالتي ref و reactive. هذا يعني أنك تستطيع إنشاء مكون يحتوي على 50 خاصية، لكن تجعل 5 منها فقط reactive، مما يقلل بشكل كبير من الحمل على نظام الـ reactivity في Vue. لكن هذا التحكم يأتي بثمن: عليك الآن إدارة الـ reactivity يدوياً، وهذا يتطلب فهماً عميقاً لكيفية عمل النظام خلف الكواليس. على سبيل المثال، إذا نسيت استخدام .value عند الوصول إلى ref داخل دالة، ستجد نفسك تتعامل مع كائن Ref بدلاً من القيمة الفعلية، وهذا خطأ شائع يؤدي إلى ساعات من الـ debugging المحبط.
// Options API - كل شيء reactive تلقائياً حتى لو لم نحتاجه
const TodoList = {
data() {
return {
todos: [],
newTodo: '',
filter: 'all',
lastUpdated: null,
// هذه الخاصية لا تُستخدم في الـ template لكنها تبقى reactive
internalCache: {}
}
},
methods: {
addTodo() {
this.todos.push(this.newTodo)
this.newTodo = ''
}
}
}
// Composition API - نتحكم بالضبط في ما هو reactive
import { ref } from 'vue'
export default {
setup() {
const todos = ref([])
const newTodo = ref('')
const filter = ref('all')
// لا حاجة لجعل internalCache reactive
const internalCache = {}
function addTodo() {
todos.value.push(newTodo.value)
newTodo.value = ''
}
// لاحظ أننا نستخدم .value داخل الدوال
return { todos, newTodo, filter, addTodo }
}
}هناك اعتقاد شائع بأن Composition API دائماً أسرع من Options API، لكن الحقيقة أكثر تعقيداً. الفرق في الأداء يظهر بوضوح في حالتين محددتين: المكونات الكبيرة ذات المنطق المعقد، والمكونات التي تحتاج إلى إعادة استخدام المنطق عبر التطبيق. في المكونات الصغيرة البسيطة، قد لا تلاحظ أي فرق في الأداء، بل قد يكون Options API أسرع أحياناً بسبب بساطة تنفيذه داخلياً. دعونا نحلل ما يحدث خلف الكواليس عندما تقوم بإنشاء مكون بكل واجهة.
عندما تقوم بتعريف مكون باستخدام Options API، يقوم Vue بإنشاء كائن داخلي يسمى "instance" يحتوي على جميع الخصائص المحددة في Options (data، methods، computed، إلخ). هذا الكائن يتم إنشاؤه مرة واحدة عند إنشاء المكون، ويتم تحديثه عند الحاجة. المشكلة تظهر عندما يكون لديك مكون ضخم يحتوي على عشرات الخصائص والدوال - الكائن الداخلي يصبح ثقيلاً، وكل تغيير في أي خاصية يؤدي إلى إعادة تقييم جميع الـ watchers المرتبطة بهذا المكون. في المقابل، Composition API يسمح لك بتقسيم المكون إلى بلوكات منطقية، كل بلوك يمكن أن يكون مستقلاً عن الآخر. هذا يعني أن تغيير خاصية في بلوك معين قد لا يؤدي إلى إعادة تقييم البلوكات الأخرى، مما يقلل من عدد العمليات التي يقوم بها نظام الـ reactivity.
// مثال على مكون ضخم مع Options API - كل شيء في مكان واحد
const UserProfile = {
data() {
return {
user: null,
posts: [],
comments: [],
friends: [],
notifications: [],
settings: {}
}
},
computed: {
recentPosts() {
return this.posts.slice(0, 5)
},
unreadNotifications() {
return this.notifications.filter(n => !n.read)
}
},
methods: {
fetchUser() { /* ... */ },
fetchPosts() { /* ... */ },
fetchComments() { /* ... */ }
},
created() {
this.fetchUser()
this.fetchPosts()
}
}
// نفس المكون مع Composition API - مقسم إلى بلوكات منطقية
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// بلوك المستخدم
const user = ref(null)
const fetchUser = async () => {
user.value = await api.fetchUser()
}
// بلوك المنشورات
const posts = ref([])
const recentPosts = computed(() => posts.value.slice(0, 5))
const fetchPosts = async () => {
posts.value = await api.fetchPosts()
}
// بلوك الإشعارات
const notificati ref([])
const unreadNotifications = computed(() =>
notifications.value.filter(n => !n.read)
)
onMounted(() => {
fetchUser()
fetchPosts()
})
return {
user, posts, recentPosts, notifications, unreadNotifications
}
}
}في المثال أعلاه، إذا قمنا بتحديث خاصية posts باستخدام Composition API، لن يتم إعادة تقييم بلوك الإشعارات لأنه مستقل تماماً. لكن في Options API، أي تغيير في أي خاصية قد يؤدي إلى إعادة تقييم جميع الـ computed properties في المكون، حتى تلك التي لا علاقة لها بالتغيير. هذا الفرق يصبح ملحوظاً عندما يكون لديك مكون يحتوي على عشرات الخصائص والدوال، كما هو الحال في لوحات التحكم الإدارية أو تطبيقات إدارة المحتوى الكبيرة.
نظام الـ reactivity في Vue يعتمد على الـ JavaScript Proxies في Vue 3، وهو أكثر كفاءة من الـ Object.defineProperty المستخدم في Vue 2. لكن حتى مع هذا التحسن، لا يزال هناك تكلفة مرتبطة بإنشاء والحفاظ على هذه الـ Proxies. في Options API، يتم إنشاء proxy واحد لكل مكون، يشمل جميع الخصائص في الـ data. هذا يعني أنه حتى الخصائص التي لا تستخدم في الـ template تبقى تحت مراقبة نظام الـ reactivity. في المقابل، Composition API يسمح لك بإنشاء proxies منفصلة لكل مجموعة من الخصائص ذات الصلة، أو حتى عدم جعل بعض الخصائص reactive على الإطلاق.
لنأخذ مثالاً عملياً: مكون يعرض بيانات مستخدم مع قائمة طويلة من المنشورات. في Options API، إذا كان لديك 100 منشور، وكل منشور يحتوي على 10 خصائص، فهذا يعني أنك تنشئ 1000 خاصية reactive، حتى لو كنت تعرض فقط 3 خصائص لكل منشور في الـ template. في Composition API، يمكنك جعل الخصائص الثلاثة التي تحتاجها فقط reactive، وتترك الباقي ككائنات عادية. هذا يقلل بشكل كبير من عدد الـ proxies التي يجب على Vue إدارتها، مما يؤدي إلى استهلاك أقل للذاكرة وأداء أفضل، خاصة في التطبيقات التي تعرض كميات كبيرة من البيانات.
// Options API - كل الخصائص reactive تلقائياً
const PostList = {
data() {
return {
posts: Array(100).fill().map((_, i) => ({
id: i,
title: `Post ${i}`,
content: `Content for post ${i}`,
author: `Author ${i % 10}`,
date: new Date(Date.now() - i * 86400000),
likes: Math.floor(Math.random() * 100),
comments: Math.floor(Math.random() * 20),
views: Math.floor(Math.random() * 1000),
tags: [`tag${i % 5}`, `tag${(i + 1) % 5}`],
isPublished: Math.random() > 0.3
}))
}
},
computed: {
// سيتم إعادة تقييم هذه الخاصية عند تغيير أي خاصية في أي منشور
publishedPosts() {
return this.posts.filter(p => p.isPublished)
}
}
}
// Composition API - نجعل الخصائص الضرورية فقط reactive
import { ref, computed } from 'vue'
export default {
setup() {
// فقط الخصائص التي نحتاجها في الـ template تصبح reactive
const posts = ref(Array(100).fill().map((_, i) => ({
id: i,
title: `Post ${i}`,
author: `Author ${i % 10}`,
likes: ref(Math.floor(Math.random() * 100)), // فقط هذه الخاصية reactive
isPublished: ref(Math.random() > 0.3) // فقط هذه الخاصية reactive
// باقي الخصائص عادية
})))
const publishedPosts = computed(() =>
posts.value.filter(p => p.isPublished.value)
)
// يمكننا تحديث الخصائص reactive دون التأثير على الباقي
function likePost(id) {
const post = posts.value.find(p => p.id === id)
if (post) post.likes.value++
}
return { posts, publishedPosts, likePost }
}
}أحد أكبر عيوب Options API هو صعوبة إعادة استخدام المنطق بين المكونات. في Vue 2، كنا نعتمد على الـ mixins لإعادة استخدام الكود، لكن هذا النهج له مشاكله الخاصة: تضارب الأسماء، صعوبة تتبع مصدر الخصائص والدوال، وعدم وضوح العلاقة بين الـ mixins المختلفة. Composition API يقدم حلاً أنيقاً لهذه المشكلة من خلال الـ Composable Functions، وهي ببساطة دوال عادية تستخرج المنطق القابل لإعادة الاستخدام وتعيد كائنات تحتوي على الـ refs والـ computed properties والدوال ذات الصلة.
في أحد المشاريع الكبيرة الذي عملت عليه، كان لدينا 15 مكوناً مختلفاً يستخدمون نفس المنطق لإدارة حالة التحميل، معالجة الأخطاء، والبحث عن البيانات. مع Options API، اضطررنا لإنشاء 3 mixins مختلفة والتعامل مع تضارب الأسماء بين الـ mixins والمكونات. عندما انتقلنا إلى Composition API، استبدلنا الـ mixins بـ 3 composable functions فقط، مما جعل الكود أكثر وضوحاً وأسهل للصيانة. ، أصبح بإمكاننا اختبار كل composable function بشكل مستقل، وهو أمر شبه مستحيل مع الـ mixins.
// مثال على composable function لإدارة حالة التحميل والأخطاء
import { ref } from 'vue'
export function useAsyncData(fetchFn) {
const data = ref(null)
const isLoading = ref(false)
const error = ref(null)
async function execute() {
isLoading.value = true
error.value = null
try {
data.value = await fetchFn()
} catch (err) {
error.value = err
} finally {
isLoading.value = false
}
}
return { data, isLoading, error, execute }
}
// استخدام الـ composable في مكون
import { useAsyncData } from './useAsyncData'
export default {
setup() {
const { data: user, isLoading, error, execute: fetchUser } = useAsyncData(() =>
api.fetchUser(1)
)
const { data: posts, execute: fetchPosts } = useAsyncData(() =>
api.fetchPosts(1)
)
return { user, posts, isLoading, error, fetchUser, fetchPosts }
}
}لاحظ كيف أن كل استخدام لـ useAsyncData مستقل تماماً عن الآخر. هذا يعني أن استدعاء fetchUser لن يؤثر على حالة posts أو العكس. في Options API، كان علينا إنشاء mixin منفصل لكل نوع من البيانات، أو استخدام نفس الـ mixin مع منطق معقد للتحكم في أي بيانات يتم تحميلها. مع Composition API، كل شيء واضح ومباشر، ويمكنك بسهولة إضافة أو إزالة حالات التحميل دون التأثير على بقية المكون.
رغم كل مميزات Composition API، هناك حالات يكون فيها Options API هو الخيار الأفضل. أولاً، المكونات البسيطة جداً التي لا تحتاج إلى إعادة استخدام المنطق أو تقسيم الكود. في هذه الحالات، قد يكون Options API أكثر وضوحاً وأسهل للفهم، خاصة للمطورين الجدد في الفريق. ثانياً، المشاريع الصغيرة أو النماذج الأولية التي تحتاج إلى تطوير سريع - Composition API يتطلب تخطيطاً أكثر للكود، مما قد يبطئ عملية التطوير الأولية.
هناك أيضاً مشكلة الـ "magic strings" التي تظهر عند استخدام Composition API مع الـ template. في Options API، عندما تستخدم خاصية في الـ template، يمكنك بسهولة تتبع مصدرها من خلال النظر إلى قسم data أو computed. لكن في Composition API، قد تجد نفسك تبحث في كامل دالة setup للعثور على مصدر خاصية معينة. هذا يمكن أن يكون محبطاً في المكونات الكبيرة، خاصة إذا لم تتبع نمط تسمية واضح للـ refs والـ computed properties.
// المشكلة: من أين تأتي هذه الخصائص؟
<template>
<div>
<h1>{{ user.name }}</h1>
<p>{{ formattedDate }}</p>
<button @click="updateProfile">Update</button>
</div>
</template>
>
import { ref, computed } from 'vue'
export default {
setup() {
// هل user هنا؟ أم في مكان آخر؟
const user = ref({ name: 'John' })
// formattedDate قد تكون computed property أو ref عادي
const formattedDate = computed(() =>
new Date().toLocaleDateString()
)
// أو ربما تأتي من composable آخر؟
function updateProfile() {
// ...
}
return { user, formattedDate, updateProfile }
}
}
</script>مشكلة أخرى شائعة مع Composition API هي نسيان استخدام .value عند الوصول إلى الـ refs داخل الدوال. هذا الخطأ سهل الوقوع فيه، خاصة للمطورين الذين اعتادوا على Options API حيث يتم الوصول إلى الخصائص مباشرة باستخدام this. في أحد المشاريع، قضيت ساعتين في البحث عن سبب عدم تحديث واجهة المستخدم عند تغيير قيمة ref، فقط لأكتشف أنني نسيت استخدام .value داخل دالة. هذا النوع من الأخطاء يمكن تجنبه باستخدام أدوات مثل ESLint مع قواعد مخصصة لـ Vue، لكن لا يزال يمثل تحدياً للمطورين الجدد.
بعد سنوات من العمل مع Vue في مشاريع مختلفة الأحجام، توصلت إلى قاعدة بسيطة: استخدم Composition API في المكونات المعقدة التي تحتاج إلى إعادة استخدام المنطق، أو عندما يكون لديك أكثر من 5 خصائص computed أو watchers. استخدم Options API في المكونات البسيطة، أو عندما يكون لديك فريق جديد على Vue ويريدون تعلم الإطار دون تعقيدات إضافية. لكن الأهم من ذلك هو الاتساق - لا تخلط بين الواجهتين في نفس المشروع دون سبب مقنع، فهذا سيؤدي إلى كود غير متسق وصعب الصيانة.
إذا كنت تبدأ مشروعاً جديداً من الصفر، أنصحك باستخدام Composition API كخيار افتراضي، مع استخدام Options API فقط للمكونات البسيطة جداً. هذا النهج سيمنحك المرونة اللازمة لإعادة استخدام المنطق وتحسين الأداء عند الحاجة، بينما يبقي الكود بسيطاً في الأماكن التي لا تحتاج إلى التعقيد. وإذا كنت تعمل على مشروع قائم يستخدم Options API، فلا داعي لإعادة كتابة كل شيء - ابدأ بإدخال Composition API تدريجياً في المكونات الجديدة أو تلك التي تحتاج إلى إعادة هيكلة، مع الحفاظ على الاتساق داخل كل مكون.
الاختيار بين Vue 3 Composition API و Options API ليس قراراً أبيض وأسود. كل واجهة لها مزاياها وعيوبها، والقرار الصحيح يعتمد على سياق مشروعك وفريقك. لكن هناك شيء واحد مؤكد: Composition API ليس مجرد موضة عابرة، بل هو تطور طبيعي لإطار عمل أصبح أكثر نضجاً وقوة. إذا كنت تعمل في مشروع كبير أو تتوقع نمواً سريعاً للتطبيق، فإن الاستثمار في تعلم واستخدام Composition API سيكون له عائد كبير على المدى الطويل. فقط تذكر أن القوة تأتي مع المسؤولية - كلما زادت المرونة التي يمنحك إياها الإطار، زاد العبء عليك لفهم كيف يعمل خلف الكواليس وتجنب الفخاخ الشائعة.
في المرة القادمة التي تفتح فيها ملف مكون جديد، اسأل نفسك: هل هذا المكون سيصبح معقداً في المستقبل؟ هل سأحتاج إلى إعادة استخدام هذا المنطق في أماكن أخرى؟ هل يمكنني تحسين أداء هذا المكون عن طريق جعل بعض الخصائص غير reactive؟ إذا كانت الإجابة على أي من هذه الأسئلة بنعم، فربما يكون Composition API هو الخيار الأفضل. وإلا، فلا تخجل من استخدام Options API - أحياناً البساطة هي أفضل حل هندسي.