Vue.js: التواصل بين المكونات

آخر تحديث: 2026-08-26

يُعد التواصل بين المكونات آلية أساسية في Vue — حيث تقوم المكونات الأم بتمرير البيانات إلى المكونات التابعة عبر الخصائص (props)، بينما تقوم المكونات التابعة بتشغيل الأحداث باستخدام emits لإخطار المكونات الأم. ويُعد فهم تدفق البيانات في الاتجاهين أمرًا أساسيًّا لبناء تطبيقات Vue جيدة.

يوفر <script setup> في Vue 3 ماكروين في وقت التحويل البرمجي، وهما defineProps وdefineEmits، واللذان يمكن استخدامهما دون الحاجة إلى الاستيراد ويدعمان استنتاج الأنواع في TypeScript بشكل كامل.

1. ما ستتعلمه



2. فخ «الربط في الاتجاهين» في عربة التسوق في التجارة الإلكترونية

(1) المشكلة: عندما يقوم مكون فرعي بتغيير إحدى خصائص prop، يقوم المكون الأصلي بإصدار خطأ

قامت أليس بتنفيذ عربة تسوق تتيح إدخال الكمية:

VUE
<!-- 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 الحل

VUE
<!-- 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>
VUE
<!-- 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) الإيرادات

بعد إصلاح تدفق البيانات:



3. الصيغة الكاملة لـ props

(1) إعلانات لـ 7 أنواع من المكونات الإضافية

VUE
<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) مثال تفصيلي

VUE
<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) تعريفات الأنواع الأولية

VUE
<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) إلزامي مقابل اختياري (؟)

TS
defineProps<{
  // Required(Default)
  user: User
  pageSize: number
  
  // Optional (add ?)
  variant?: 'primary' | 'secondary'
  showIcon?: boolean
}>()

(3) توفر وظيفة withDefaults القيم الافتراضية

VUE
<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 أنواع من عبارات الأحداث

VUE
<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 طرق للتشغيل

VUE
<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) الاستماع للمكون الأصلي

VUE
<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

VUE
<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) المبادئ الأساسية

100%
graph LR
    A[Parent Component data] -->|props| B[Child component]
    B -->|emit| A
    
    style A fill:#42b883
    style B fill:#42b883

(2) 5 استثناءات

VUE
<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 أنماط سيئة

JS
// ❌ 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) السلوك الافتراضي

VUE
<!-- 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) تعطيل التمرير التلقائي

VUE
<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

VUE
<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) الكاملة

VUE 📖 للعرض فقط
<!-- 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>
56 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

▶ مثال: 2. المكون الأصلي الذي يستخدم ProductCard

VUE
<!-- 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

VUE
<!-- 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)

VUE
<!-- 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


❓ أسئلة شائعة

س هل يمكن تعديل الخصائص (props) مباشرةً؟
ج لا. وفقًا لمبدأ تدفق البيانات أحادي الاتجاه في Vue، فإن الخصائص (props) هي للقراءة فقط. لتعديل البيانات، يجب على المكون الفرعي إصدار حدث حتى يتمكن المكون الأصلي من إجراء التغيير. سيؤدي التعديل المباشر للخصائص (props) إلى ظهور تحذير من Vue.
س كيف يمكنني تمرير بيانات معقدة (كائنات متداخلة)؟
ج قم بتمرير المرجع مباشرةً؛ حيث تتولى Vue معالجة الربط العميق تلقائيًّا. عندما تقوم بتعديل خاصية داخل الكائن، سيكتشف المكون الأصلي التغيير (الربط العميق)، لكن يجب ألا تقوم المكونات الفرعية بتعديلها مباشرةً؛ بل يجب عليها استخدام 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 في هذا الدرس للحصول على شرح مفصل.
س كيف يمكنني مراقبة التغييرات التي تطرأ على خصائص (props) في مكون فرعي؟
ج استخدم watch(() => props.xxx, (newVal, oldVal) => {...}). احرص على عدم تعديل الخصائص داخل عملية المراقبة، وإلا ستدخل في حلقة لا نهائية.
س متى ينبغي استخدام useAttrs؟
ج في ثلاث حالات: (1) يكون مطلوبًا للمكونات متعددة الجذور (Fragments)؛ (2) عندما تريد تمرير السمات إلى عناصر غير جذرية؛ (3) عندما تريد التحكم صراحةً في مكان ربط السمات. ولا حاجة إليه في المكونات العادية ذات الجذر الواحد.
س ماذا يحدث إذا فشل التحقق من صحة المعلمات؟
ج في بيئة التطوير، يصدر Vue تحذيرًا؛ أما في بيئة الإنتاج، فيفشل دون إظهار أي رسالة. يُنصح بأن يُرجع مُحقق الصحة false (خطأ) أو سلسلة نصية (رسالة خطأ).

📖 ملخص


📝 تمارين

  1. أسئلة أساسية (مستوى الصعوبة: ⭐)

    تنفيذ مكون «زر» بسيط:

    • المعلمات: text (سلسلة)، variant (سلسلة، 'primary'/'success'/'danger')، disabled (قيمة منطقية)
    • هناك 3 أشكال تتوافق مع 3 فئات CSS
    • إصدار الحدث click (بدون معلمات)
    • اختبار المتغيرات الثلاثة في المكون الأصلي
  2. مسائل متقدمة (مستوى الصعوبة: ⭐⭐)

    تنفيذ مكون إدخال محدود ثنائي الاتجاه:

    • الملف الفرعي CustomInput.vue: props: modelValue, label
    • استخدام «v-model» كأسلوب برمجي مبسط
    • emit update:modelValue
    • المكون الأصلي: يعرض مدخلات المستخدم في الوقت الفعلي
  3. مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)

    تطبيق نظام شامل لـ «بطاقة المنتج + المفضلة»:

    1. ProductCard.vue: الخصائص الكاملة (product/showStockBadge/maxStock) + الإشارات (add-to-cart/toggle-favorite)
    2. App.vue: قائمة المنتجات + status المفضلة
    3. زر «المفضلة»: إرسال خاصية toggle-favorite؛ يقوم المكون الأصلي بتحديث status «المفضلة»
    4. تنفيذ المكونات متعددة الجذور باستخدام useAttrs وinheritAttrs (دون الإخلال بالتمرين)
    5. تم التحقق منها من قبل 3 مدققين على الأقل (البريد الإلكتروني/عنوان URL/قائمة التعداد)
    6. الخصائص ذات الأنواع القوية في TypeScript
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%