Vue.js: التواصل بين المكونات
آخر تحديث: 2026-10-20
يُعد التواصل بين المكونات آلية أساسية في Vue — حيث تقوم المكونات الأم بتمرير البيانات إلى المكونات التابعة عبر الخصائص (props)، بينما تقوم المكونات التابعة بتشغيل الأحداث باستخدام emits لإخطار المكونات الأم. ويُعد فهم تدفق البيانات في الاتجاهين أمرًا أساسيًّا لبناء تطبيقات Vue جيدة.
يوفر <script setup> في Vue 3 ماكروين في وقت التحويل البرمجي، وهما defineProps وdefineEmits، واللذان يمكن استخدامهما دون الحاجة إلى الاستيراد ويدعمان استنتاج الأنواع في TypeScript بشكل كامل.
1. ما ستتعلمه
- قواعد التكوين الكاملة لـ props: الأنواع، والحقول الإلزامية، والقيم الافتراضية، وأدوات التحقق من الصحة
- طرق متعددة لإعلان الخصائص (أثناء التشغيل / حسب النوع / مبسطة)
- يصدر إعلانات الأحداث وعمليات التحقق من الصحة
- استدلال الأنواع في TypeScript (
defineProps<T>()) - تمرير الخصائص (inheritAttrs / useAttrs)
- مبدأ تدفق البيانات في اتجاه واحد
- 5 أخطاء شائعة وأفضل الممارسات
2. فخ «الربط في الاتجاهين» في عربة التسوق في التجارة الإلكترونية
(1) المشكلة: عندما يقوم مكون فرعي بتغيير إحدى خصائص prop، يقوم المكون الأصلي بإصدار خطأ
قامت أليس بتنفيذ عربة تسوق تتيح إدخال الكمية:
تُظهر Vue رسالة خطأ:
[Vue warn] Set operation on key "quantity" failed: target is readonly.
بعد قضاء ساعة في تصحيح الأخطاء، اكتشف تشارلي ما يلي:
"أليس، إجمالي سلة التسوق الخاصة بالعنصر الأصلي لا يتم تحديثه عندما أقوم بتغيير الكمية. يقوم Vue بتعديل الخاصية (prop)، لكن العنصر الأصلي لا يدرك ذلك."
(2) تدفق البيانات أحادي الاتجاه في Vue + emit الحل
الآن يتم تدفق التغييرات بشكل صحيح: يقوم «الفرعي» بإرسال التغييرات → يتلقى «الأصلي» التغييرات → يقوم «الأصلي» بتحديث البيانات → يعيد تمريرها إلى «الفرعي».
(3) الإيرادات
بعد إصلاح تدفق البيانات:
- تحذير بشأن عدد المشاهدات: 100/يوم → 0
- اتساق البيانات: المزامنة في الوقت الفعلي مع المكون الأصلي
- إمكانية التصحيح: تدفق البيانات أحادي الاتجاه، مما يسهل تتبعه
- قابلية الاختبار: يمكن اختبار المكونات بشكل مستقل للتحقق من خصائصها (props) وعمليات الإرسال (emit)
3. الصيغة الكاملة لـ props
(1) إعلانات لـ 7 أنواع من المكونات الإضافية
<script setup>
defineProps({
// 1. Basic Types
name: String,
age: Number,
active: Boolean,
// 2. Complex Types
user: Object,
items: Array,
// 3. Multiple Types(Any)
id: [String, Number],
// 4. Required
title: { type: String, required: true },
// 5. Default value
pageSize: { type: Number, default: 20 },
// 6. Custom Validation
email: {
type: String,
validator: (val) => val.includes('@')
},
// 7. Function Default Values(The default value must be a function.)|
createdAt: {
type: Date,
default: () => new Date()
}
})
</script>
(2) مثال تفصيلي
<script setup>
defineProps({
// String
title: String,
// Numbers + Default value + Verification
pageSize: {
type: Number,
default: 20,
validator: (val) => val > 0 && val <= 100
},
// Boolean(Note:The default value for falsy Values should be in function form)
isVip: {
type: Boolean,
default: false
},
// Array Default Values(It must be returned as a function)
tags: {
type: Array,
default: () => ['vue', 'javascript']
},
// Object Default Values(It must be returned as a function)
user: {
type: Object,
default: () => ({ name: 'Guest', age: 0 })
},
// Required + Custom Validation
email: {
type: String,
required: true,
validator: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
}
})
</script>
(3) 6 قواعد للقيم الافتراضية
| القاعدة | المثال |
|---|---|
| الأنواع الأساسية | default: 20 |
| المصفوفات/الكائنات | default: () => [] |
| null | default: null |
| غير محدد | لا حاجة إلى القيمة الافتراضية |
| دليل الوظائف | لا تستخدم default: fn؛ استخدم () => fn() |
| Symbol / BigInt | تُرجع الدالة المصنعية |
▶ مثال: ProductCard.vue — الخصائص (props) والإشارات (emits) الكاملة
▶ جرّب الكودالإخراج:
stock = 50 → زر Add to Cart مفعّل بلا شارة
stock = 5 → شارة Only 5 left
stock = 0 → Out of Stock والزر معطّل
4. الخصائص ذات الأنواع القوية في TypeScript (موصى بها لـ Vue 3)
(1) تعريفات الأنواع الأولية
<script setup lang="ts">
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
defineProps<{
user: User
size?: 'small' | 'medium' | 'large'
showEmail?: boolean
}>()
</script>
(2) إلزامي مقابل اختياري (؟)
defineProps<{
// Required(Default)
user: User
pageSize: number
// Optional (add ?)
variant?: 'primary' | 'secondary'
showIcon?: boolean
}>()
(3) توفر وظيفة withDefaults القيم الافتراضية
<script setup lang="ts">
interface Props {
user: User
size?: 'small' | 'medium' | 'large'
showEmail?: boolean
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
showEmail: true
})
</script>
(4) مقارنة بين إعلانات وقت التشغيل وإعلانات الأنواع
| البعد | وقت التشغيل defineProps({}) |
النوع defineProps<T>() |
|---|---|---|
| استدلال الأنواع | ضعيف | قوي (الإكمال التلقائي في بيئة تطوير المتكاملة) |
| القيمة الافتراضية | القيمة الافتراضية المباشرة | غلاف withDefaults |
| التحقق من الصحة | دالة التحقق | فحص وقت التحويل البرمجي في TypeScript |
| موصى به | المشاريع الصغيرة/JS الخالص | المشاريع الكبيرة/مشاريع TS |
5. الصيغة الكاملة لـ emit
(1) 5 أنواع من عبارات الأحداث
<script setup>
// 1. A Brief Statement
const emit = defineEmits(['click', 'submit', 'cancel'])
// 2. Parameter Validation
const emit = defineEmits({
addToCart: (productId) => typeof productId === 'number',
remove: (id) => typeof id === 'string'
})
// 3. TypeScript Type
const emit = defineEmits<{
'add-to-cart': [productId: number]
'remove-item': [id: string, reason: string]
'update': [id: number, data: object]
}>()
</script>
(2) 5 طرق للتشغيل
<script setup>
const emit = defineEmits(['click', 'submit', 'add-to-cart'])
// 1. Simple Trigger
emit('click')
// 2. Passing Parameters
emit('add-to-cart', 123)
// 3. Pass multiple parameters
emit('submit', { name: 'Alice' }, 2026)
// 4. Conditional Trigger
if (isValid) emit('submit')
// 5. Functional Triggering
function handleAdd() {
emit('add-to-cart', props.product.id)
}
</script>
(3) الاستماع للمكون الأصلي
(4) التحقق من صحة النوع لـ emit
<script setup>
const emit = defineEmits({
// ✅ Verification:id It must be number
remove: (id) => typeof id === 'number' || 'Validation failed',
// ❌ Throws Error: When parameters do not match
// remove: 'invalid', // Wrong: It must be a function
})
</script>
▶ مثال: الربط ثنائي الاتجاه في نموذج v-model
<!-- Child component CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- Using Parent Components v-model -->
<template>
<CustomInput v-model="searchQuery" />
<p>Query: {{ searchQuery }}</p>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from '@/components/CustomInput.vue'
const searchQuery = ref('')
</script>
الإخراج:
الحقل و Query فارغان ابتداءً
كتابة iPhone → Query: iPhone
emit update:modelValue يحدّث searchQuery في الأب
6. مبدأ تدفق البيانات في اتجاه واحد
(1) المبادئ الأساسية
graph LR
A[Parent Component data] -->|props| B[Child component]
B -->|emit| A
style A fill:#42b883
style B fill:#42b883
- لا يمكن للبيانات أن تتدفق إلا من العنصر الأصلي إلى العنصر التابع (تتدفق الخصائص من الأعلى إلى الأسفل)
- لا يمكن للأحداث أن تتدفق إلا من العنصر الفرعي إلى العنصر الأصلي (الانبعاث التصاعدي)
- لا يمكن للمكونات الفرعية تعديل الخصائص (props) بشكل مباشر
(2) 5 استثناءات
<script setup>
const props = defineProps({ user: Object })
// ❌ Wrong: Edit prop directly
props.user.name = 'Bob' // Vue Warning!
// ✅ Exception 1:prop When it is a reference type,Can be replaced with a new object
// The parent component passes a reference,Child components cannot change their references,But it's possible emit Let Father Change It
// ✅ Exception 2:prop Initial values can be saved locally
const localUser = ref({ ...props.user })
// Then make the changes localUser(Does not affect the parent)
// ✅ Exception 3: Use computed Derivative
const userName = computed(() => props.user.name)
// ✅ Exception 4: Use v-model (Syntax Sugar)
// <Child v-model="value" /> equivalent to :value + @update:value
// ✅ Exception 5:provide/inject Across levels
// Parent provide, Child inject (Learned in Phase 2.5)
</script>
(3) 5 أنماط سيئة
// ❌ Anti-pattern 1:Edit directly prop
props.user.name = 'Bob'
// ❌ Anti-pattern 2:Use in child components watch Edit prop
watch(() => props.value, (val) => { props.value = val * 2 })
// ❌ Anti-pattern 3: Two-way bound ref passthrough (Not recommended)
const localRef = ref(props.value)
watch(localRef, (val) => emit('update', val))
// ❌ Anti-pattern 4:Usage v-model Does not follow the naming convention
// v-model Expectations update:xxx Event,Custom event names must be consistent
emit('change', val) // ❌ Parent Component @change Will not be triggered
emit('update:value', val) // ✅ v-model Supporting
// ❌ Anti-pattern 5:Managing the parent component's data directly within a child component
emit('update', { ...props.user, name: 'Bob' }) // Wrong: Should only pass ID, Parent updates itselff
7. تمرير الخصائص: inheritAttrs و useAttrs
(1) السلوك الافتراضي
(2) تعطيل التمرير التلقائي
(3) الاستخدام الصريح لـ useAttrs
(4) 5 حالات استخدام
| السيناريو | الطريقة |
|---|---|
| الافتراضي | لا يلزم اتخاذ أي إجراء؛ يتم تطبيق الفئة تلقائيًا على الجذر |
| عنصر جذر مخصص | useAttrs() ربط صريح |
| التوسع إلى العناصر غير الجذرية | inheritAttrs: false + v-bind="$attrs" |
| مكونات متعددة (شظايا) | يجب استخدام inheritAttrs: false + ربط صريح |
| تحسين الأداء | تعطيل ميزة «التمرير المباشر» لتجنب سمات DOM غير الضرورية |
▶ مثال: تمرير الخصائص (useAttrs Pass-Through)
▶ جرّب الكودالإخراج:
label يظهر فوق الحقل: Name
placeholder و class يصلان إلى input عبر $attrs
inheritAttrs: false يمنع وضعهما على الجذر
(1) 📊 5. مرجع سريع لـ 5 أخطاء شائعة
| الخطأ | الأعراض | الحل |
|---|---|---|
| تعديل الخاصية مباشرةً | تحذير Vue | استخدم emit للسماح للعنصر الأصلي بتعديلها |
| المصفوفات/الكائنات: تحتوي خصائص (props) على قيم افتراضية كمرجع | مشتركة بين عدة مكونات | default: () => [] المصنع |
| عدم تطابق اسم الحدث في نموذج V | لا يتلقاه العنصر الأصلي | استخدم update:xxx |
| يُرجع validator سلسلة | لا يُصدر Vue خطأً | يُرجع true/false |
| التمرير إلى مكونات متعددة | تحذير Vue | inheritAttrs: false |
(2) 📊 6. خمسة سيناريوهات رئيسية للتحقق من صحة قيم الخصائص
| السيناريو | التحقق |
|---|---|
| البريد الإلكتروني | val => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) |
| عنوان URL | val => /^https?:\/\//.test(val) |
| قيمة التعداد | val => ['a', 'b', 'c'].includes(val) |
| النطاق | val => val >= 0 && val <= 100 |
| كائن مخصص | val => val.id && val.name |
8. تمرين شامل: المكوّن الأب الذي يستخدم ProductCard مع props و emits
▶ مثال: المكون الأصلي الذي يستخدم ProductCard
<!-- src/views/ProductListView.vue -->
<template>
<div class="grid">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
:show-stock-badge="true"
@add-to-cart="handleAddToCart"
@quick-view="handleQuickView"
@toggle-favorite="handleToggleFavorite"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ProductCard from '@/components/ProductCard.vue'
import { useCart } from '@/composables/useCart'
const products = ref([
{ id: 1, name: 'iPhone', price: 999, stock: 50, image: 'iphone.jpg' },
{ id: 2, name: 'MacBook', price: 2499, stock: 0, image: 'macbook.jpg' }
])
const { addToCart } = useCart()
function handleAddToCart(productId) {
addToCart(productId)
}
function handleQuickView(productId) {
console.log('Quick view:', productId)
}
function handleToggleFavorite(productId, isFavorite) {
console.log('Toggle:', productId, isFavorite)
}
</script>
الإخراج:
iPhone: الزر مفعّل، MacBook: Out of Stock
add-to-cart يمرّر المعرّف إلى addToCart
quick-view و toggle-favorite يطبعان في console
❓ أسئلة شائعة
emit للسماح للمكون الأصلي بتحديث الكائن أو الخاصية بالكامل.defineProps وprops؟<script setup>، استخدم الماكرو defineProps() (لا يتطلب الاستيراد)؛ أما في واجهة برمجة تطبيقات الخيارات (Options API)، فاستخدم الخيار props: {}. الخياران متكافئان؛ نوصي باستخدام setup + defineProps.emit بشكل غير متزامن في مكون فرعي؟emit هي دعوة دالة متزامنة، ولكن يمكن تغليفها في setTimeout أو في Promise: setTimeout(() => emit('done'), 1000).v-model؟props و emit. <Child v-model="x" /> يعادل :modelValue="x" @update:modelValue="x = $event". انظر المثال 3 في هذا الدرس للحصول على شرح مفصل.watch(() => props.xxx, (newVal, oldVal) => {...}). احرص على عدم تعديل الخصائص داخل عملية المراقبة، وإلا ستدخل في حلقة لا نهائية.useAttrs؟false (خطأ) أو سلسلة نصية (رسالة خطأ).📖 ملخص
- الدعائم: استخدم
defineProps()عند الانتقال من العنصر الأصلي إلى العنصر الفرعي، وdefineEmits()عند الانتقال من العنصر الفرعي إلى العنصر الأصلي - 7 أنواع من المعلمات الإضافية: أساسية / معقدة / متعددة الأنواع / إلزامية / افتراضية / للتحقق من الصحة / مصنعية
- يستخدم TypeScript
defineProps<T>()للكتابة القوية +withDefaults()مع القيم الافتراضية - 5 أنواع من المشغلات + التحقق من صحة النوع
- مبدأ تدفق البيانات أحادي الاتجاه: تتدفق البيانات إلى الأسفل، بينما تتدفق الأحداث إلى الأعلى
- 5 أنماط سيئة: التعديل المباشر لـ props / تعديل props عبر
watch/ تسرب بيانات المرجع في الاتجاهين / عدم تطابق أسماء الأحداث في نموذج v-model / إدارة الطفل لبيانات الوالد - inheritAttrs / useAttrs لتمرير الخصائص (Props Passthrough)
v-modelهو تعبير مبسط لـpropsوemit
📝 تمارين
-
أسئلة أساسية (مستوى الصعوبة: ⭐)
تنفيذ مكون «زر» بسيط:
- المعلمات:
text(سلسلة)،variant(سلسلة، 'primary'/'success'/'danger')،disabled(قيمة منطقية) - هناك 3 أشكال تتوافق مع 3 فئات CSS
- إصدار الحدث
click(بدون معلمات) - اختبار المتغيرات الثلاثة في المكون الأصلي
- المعلمات:
-
مسائل متقدمة (مستوى الصعوبة: ⭐⭐)
تنفيذ مكون إدخال محدود ثنائي الاتجاه:
- الملف الفرعي CustomInput.vue: props:
modelValue,label - استخدام «
v-model» كأسلوب برمجي مبسط - emit
update:modelValue - المكون الأصلي: يعرض مدخلات المستخدم في الوقت الفعلي
- الملف الفرعي CustomInput.vue: props:
-
مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)
تطبيق نظام شامل لـ «بطاقة المنتج + المفضلة»:
- ProductCard.vue: الخصائص الكاملة (product/showStockBadge/maxStock) + الإشارات (add-to-cart/toggle-favorite)
- App.vue: قائمة المنتجات + status المفضلة
- زر «المفضلة»: إرسال خاصية toggle-favorite؛ يقوم المكون الأصلي بتحديث status «المفضلة»
- تنفيذ المكونات متعددة الجذور باستخدام
useAttrsوinheritAttrs(دون الإخلال بالتمرين) - تم التحقق منها من قبل 3 مدققين على الأقل (البريد الإلكتروني/عنوان URL/قائمة التعداد)
- الخصائص ذات الأنواع القوية في TypeScript