Vue.js: التواصل بين المكونات
آخر تحديث: 2026-08-26
يُعد التواصل بين المكونات آلية أساسية في Vue — حيث تقوم المكونات الأم بتمرير البيانات إلى المكونات التابعة عبر الخصائص (props)، بينما تقوم المكونات التابعة بتشغيل الأحداث باستخدام emits لإخطار المكونات الأم. ويُعد فهم تدفق البيانات في الاتجاهين أمرًا أساسيًّا لبناء تطبيقات Vue جيدة.
يوفر <script setup> في Vue 3 ماكروين في وقت التحويل البرمجي، وهما defineProps وdefineEmits، واللذان يمكن استخدامهما دون الحاجة إلى الاستيراد ويدعمان استنتاج الأنواع في TypeScript بشكل كامل.
1. ما ستتعلمه
- قواعد التكوين الكاملة لـ props: الأنواع، والحقول الإلزامية، والقيم الافتراضية، وأدوات التحقق من الصحة
- طرق متعددة لإعلان الخصائص (أثناء التشغيل / حسب النوع / مبسطة)
- يصدر إعلانات الأحداث وعمليات التحقق من الصحة
- استدلال الأنواع في TypeScript (
defineProps<T>()) - تمرير الخصائص (inheritAttrs / useAttrs)
- مبدأ تدفق البيانات في اتجاه واحد
- 5 أخطاء شائعة وأفضل الممارسات
2. فخ «الربط في الاتجاهين» في عربة التسوق في التجارة الإلكترونية
(1) المشكلة: عندما يقوم مكون فرعي بتغيير إحدى خصائص prop، يقوم المكون الأصلي بإصدار خطأ
قامت أليس بتنفيذ عربة تسوق تتيح إدخال الكمية:
<!-- CartItem.vue Child component -->
<template>
<input :value="quantity" @input="quantity = $event.target.value">
</template>
<script setup>
// ❌ The "Broken" Version:Edit directly prop
const props = defineProps({ quantity: Number })
function update() {
props.quantity++ // ❌ Vue Warning:Avoid making direct changes prop
}
</script>
تُظهر Vue رسالة خطأ:
[Vue warn] Set operation on key "quantity" failed: target is readonly.
بعد قضاء ساعة في تصحيح الأخطاء، اكتشف تشارلي ما يلي:
"أليس، إجمالي سلة التسوق الخاصة بالعنصر الأصلي لا يتم تحديثه عندما أقوم بتغيير الكمية. يقوم Vue بتعديل الخاصية (prop)، لكن العنصر الأصلي لا يدرك ذلك."
(2) تدفق البيانات أحادي الاتجاه في Vue + emit الحل
<!-- CartItem.vue Child component -->
<template>
<div>
<input :value="quantity" @input="$emit('update:quantity', +$evento.target.value)">
</div>
</template>
<script setup>
const props = defineProps({ quantity: Number })
const emit = defineEmits(['update:quantity'])
</script>
<!-- Cart.vue Parent Component -->
<template>
<CartItem
v-for="item in items"
:key="item.id"
:quantity="item.quantity"
@update:quantity="(val) => item.quantity = val"
/>
</template>
الآن يتم تدفق التغييرات بشكل صحيح: يقوم «الفرعي» بإرسال التغييرات → يتلقى «الأصلي» التغييرات → يقوم «الأصلي» بتحديث البيانات → يعيد تمريرها إلى «الفرعي».
(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 | تُرجع الدالة المصنعية |
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) الاستماع للمكون الأصلي
<template>
<ChildComponent
@click="handleClick"
@add-to-cart="handleAdd"
@submit="handleSubmit"
/>
</template>
<script setup>
function handleClick() { /* No parameters */ }
function handleAdd(productId) { /* 1 parameter */ }
function handleSubmit(data, year) { /* Multiple parameters */ }
</script>
(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>
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) السلوك الافتراضي
<!-- Parent.vue -->
<template>
<ChildComponent class="parent-class" :title="title" />
</template>
<!-- ChildComponent.vue - By default, messages are received automatically. class="parent-class" -->
<template>
<div> <!-- Automatically available class="parent-class" -->
<h3>{{ title }}</h3>
</div>
</template>
(2) تعطيل التمرير التلقائي
<script setup>
defineOptions({ inheritAttrs: false })
</script>
<template>
<!-- class It will not be automatically applied to the root element -->
<div class="my-class">
<h3>Title</h3>
</div>
</template>
(3) الاستخدام الصريح لـ useAttrs
<script setup>
import { useAttrs } from 'vue'
defineOptions({ inheritAttrs: false })
const attrs = useAttrs()
</script>
<template>
<div>
<input v-bind="attrs" />
<!-- Pass the value from the parent component class/style/Manually bind other properties to input -->
</div>
</template>
(4) 5 حالات استخدام
| السيناريو | الطريقة |
|---|---|
| الافتراضي | لا يلزم اتخاذ أي إجراء؛ يتم تطبيق الفئة تلقائيًا على الجذر |
| عنصر جذر مخصص | useAttrs() ربط صريح |
| التوسع إلى العناصر غير الجذرية | inheritAttrs: false + v-bind="$attrs" |
| مكونات متعددة (شظايا) | يجب استخدام inheritAttrs: false + ربط صريح |
| تحسين الأداء | تعطيل ميزة «التمرير المباشر» لتجنب سمات DOM غير الضرورية |
8. مثال كامل: خصائص/إشارات بطاقة المنتج في التجارة الإلكترونية
▶ مثال: 1. ProductCard.vue — الخصائص (props) والإشارات (emits) الكاملة
<!-- src/components/ProductCard.vue -->
<template>
<div :class="['product-card', { 'out-of-stock': !inStock }]">
<img :src="product.image" :alt="product.name">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
<span v-if="lowStock" class="badge">Only {{ product.stock }} left</span>
<button :disabled="!inStock" @click="handleAddToCart">
{{ inStock ? 'Add to Cart' : 'Out of Stock' }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Product {
id: number
name: string
price: number
stock: number
image: string
}
// props Complete Definition
const props = defineProps({
product: {
type: Object as () => Product,
required: true,
validator: (val: Product) => val.id && val.name
},
showStockBadge: { type: Boolean, default: true }
})
// emits Complete Definition
const emit = defineEmits({
'add-to-cart': (productId: number) => typeof productId === 'number',
'quick-view': (productId: number) => typeof productId === 'number',
'toggle-favorite': (productId: number, isFavorite: boolean) =>
typeof productId === 'number' && typeof isFavorite === 'boolean'
})
// Computed Properties
const inStock = computed(() => props.product.stock > 0)
const lowStock = computed(() => props.product.stock > 0 && props.product.stock < 10)
// Event Handling
function handleAddToCart() {
if (inStock.value) {
emit('add-to-cart', props.product.id)
}
}
</script>
<style scoped>
.product-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
transition: all 0.2s;
}
.product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.out-of-stock { opacity: 0.6; }
.badge { background: #f59e0b; color: white; padding: 2px 8px; border-radius: 4px; }
.price { color: #42b883; font-weight: bold; }
</style>
▶ مثال: 2. المكون الأصلي الذي يستخدم 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>
▶ مثال: 3. الربط ثنائي الاتجاه في نموذج 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>
▶ مثال: 4. تمرير الخصائص (useAttrs Pass-Through)
<!-- CustomInput.vue - Multi-element components -->
<template>
<!-- Multiple elements must be used with inheritAttrs: false -->
<label>{{ label }}</label>
<input v-bind="$attrs" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<script setup>
defineOptions({ inheritAttrs: false })
defineProps({
modelValue: String,
label: String
})
defineEmits(['update:modelValue'])
</script>
<!-- Parent Component -->
<CustomInput v-model="name" label="Name" placeholder="Enter name..." class="input-field" />
<!-- Placeholder and class will be auto-applied to input (No label) -->
▶ مثال: 5. مرجع سريع لـ 5 أخطاء شائعة
| الخطأ | الأعراض | الحل |
|---|---|---|
| تعديل الخاصية مباشرةً | تحذير Vue | استخدم emit للسماح للعنصر الأصلي بتعديلها |
| المصفوفات/الكائنات: تحتوي خصائص (props) على قيم افتراضية كمرجع | مشتركة بين عدة مكونات | default: () => [] المصنع |
| عدم تطابق اسم الحدث في نموذج V | لا يتلقاه العنصر الأصلي | استخدم update:xxx |
| يُرجع validator سلسلة | لا يُصدر Vue خطأً | يُرجع true/false |
| التمرير إلى مكونات متعددة | تحذير Vue | inheritAttrs: false |
▶ مثال: 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 |
❓ أسئلة شائعة
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