Vue.js: التفاعل بعمق
آخر تحديث: 2026-08-26
إن فهم المبادئ الكامنة وراء التفاعلية في Vue 3 سيمكّنك من كتابة كود أكثر كفاءة وتصحيح مشكلات التفاعلية المعقدة. يتعمق هذا الدرس في التنفيذ الأساسي لنظام التفاعلية في Vue 3: معترضات البروكسي، وحل التبعيات، ومحفزات التحديث، والآليات الداخلية لـ refs، والخصائص المحسوبة، وwatches.
يحل Vue 3 محل Object.defineProperty في Vue 2 باستخدام Proxy في ES6، مما يوفر دعمًا أصليًّا للمصفوفات و«Maps» و«Sets»، ويحقق زيادة في الأداء بمقدار الضعف. وبمجرد فهم كيفية عمله، يمكنك تجنب خمسة أخطاء شائعة تتعلق بالبيانات التفاعلية.
1. ما ستتعلمه
- كيف يقوم الوكيل باعتراض عمليات «get» و«set»
- دور واجهة برمجة التطبيقات (API) Reflect في البرمجة التفاعلية
- العملية الأساسية: التتبع (جمع التبعيات) + المشغل (تشغيل عملية التحديث)
- التنفيذ الداخلي لـ
refوreactiveوcomputed - الاختلافات الداخلية بين watch و watchEffect
- تحسين الأداء: shallowRef / markRaw
- 5 أسباب جذرية شائعة لفشل التصميم المتجاوب
2. حادثة «تجميد» تتعلق بجدول مكون من 100,000 صف
(1) المشكلة: تؤدي قائمة تضم 100,000 عنصر إلى تجميد الصفحة بعد تضمينها في tag ref
قامت أليس بتحميل جدول منتجات مكون من 10,000 صفٍّ للمسؤول:
// ❌ The "Broken" Version:deep reactive Processing 10 Ten Thousand Lines
import { ref } from 'vue'
const products = ref([
{ id: 1, name: 'Product 1', price: 99, ... },
// ... 10,000 items ...
])
// Vue For each property,All nested objects are created Proxy
// Memory Usage:100MB+
// First Render: 3s
// Scroll:10 FPS(Lag)
يُظهر شاشة مراقبة الأداء ما يلي:
"10,000 صف × 4 خصائص متداخلة × عبء الوكيل = 40,000 وكيل. كل عملية وصول إلى خاصية تؤدي إلى تشغيل عملية التتبع. الذاكرة: 100 ميغابايت. معدل الإطارات في الثانية: 10."
(2) حل «shallowRef» و«markRaw» في Vue
import { shallowRef, markRaw } from 'vue'
// ✅ shallowRef:Follow Only .value Replace All
const products = shallowRef([
{ id: 1, name: 'Product 1' },
// ... 10,000 item (Will not be Proxy-wrapped) ...
])
// ✅ markRaw:The tagged object never responds(Fastest)
const staticConfig = markRaw({
apiUrl: 'https://api.example.com',
version: '1.0'
})
// Replace All
products.value = await fetchProducts() // Trigger only 1 Next Update
(3) الإيرادات
بعد التحسين:
- عدد الوكلاء: 40,000 → 0 (لا تنشئ shallowRef أي وكلاء)
- استخدام الذاكرة: 100 ميغابايت → 5 ميغابايت
- العرض الأول: 3 ثوانٍ → 200 مللي ثانية
- معدل الإطارات في الثانية (FPS): 10 → 60 (سلس)
3. اعتراض الوكيل: حجر الزاوية في تفاعلية Vue 3
(1) ما المقصود بالوكيل؟
الوكيل (Proxy) هو غلاف كائن تم تقديمه في ES6 — ويمكنه اعتراض 13 نوعًا من عمليات الكائنات (get، set، has، deleteProperty، إلخ).
// Basics Proxy Example
const target = { name: 'Alice', age: 25 }
const proxy = new Proxy(target, {
get(target, key) {
console.log(`Read ${key}`)
return Reflect.get(target, key)
},
set(target, key, value) {
console.log(`Write ${key} = ${value}`)
return Reflect.set(target, key, value)
}
})
proxy.name // Output "Read name",Back 'Alice'
proxy.age = 26 // Output "Write age = 26"
(2) التنفيذ التفاعلي في Vue 3 (نسخة مبسطة)
// Vue 3 Inside reactivity A Simplified Implementation of a Package
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 1. Dependency Collection:Track who is using this property
track(target, key)
// 2. Recursive Wrapping:Nested objects also become reactive
const value = Reflect.get(target, key, receiver)
if (isObject(value)) {
return reactive(value)
}
return value
},
set(target, key, value, receiver) {
// 1. Setting Value
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 2. Trigger an update:Notify all locations where this property is used
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
}
(3) Proxy مقابل Object.defineProperty
| الجانب | Object.defineProperty (Vue 2) | Proxy (Vue 3) |
|---|---|---|
| دعم المصفوفات | يتطلب معالجة خاصة | دعم أصلي |
| خريطة/مجموعة | ❌ | ✅ |
| إضافة خاصية جديدة | يتطلب استخدام Vue.set | Auto-react |
| الأداء | المتوسط | أفضل (الوكيل البطيء) |
| دعم المتصفحات | IE9 وما بعده | غير مدعوم في IE (يتطلب استخدام «بوليفيل») |
4. التتبع + التشغيل: تجميع التبعيات وتشغيل التحديثات
(1) مخطط التدفق الكامل
sequenceDiagram
participant T as Template
participant C as Component
participant R as Reactive<br/>(Proxy)
participant W as WeakMap<br/>(target → Map)
participant D as Data
T->>C: 1. Render (Read count)
C->>R: 2. get count
R->>W: 3. track(target, 'count')
W->>W: 4. Record the current activeEffect
R->>D: 5. Read 0
R-->>C: 6. Return 0
C-->>T: 7. Display "Stock: 0"
Note over D,T: The user clicks the button
D->>R: 8. set count = 1
R->>W: 9. trigger(target, 'count')
W->>W: 10. Find all dependencies
R->>C: 11. Scheduling Update
C-->>T: 12. Display "Stock: 1"
(2) بنية تخزين التبعيات العالمية
// Vue 3 Internal Data Structures
const targetMap = new WeakMap()
// targetMap = { obj1: Map1, obj2: Map2, ... }
// Map1 = { key1: Set1, key2: Set2, ... }
// Set1 = Set<activeEffect1, activeEffect2, ...>
function track(target, key) {
// 1. Get target Corresponding Map
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 2. Get key Corresponding Set
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 3. Record the currently active effect
if (activeEffect) {
dep.add(activeEffect)
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
// Notify all dependencies to re-execute
dep.forEach(effect => effect.run())
}
}
(3) 5 نقاط رئيسية
| النقاط الرئيسية | الوصف |
|---|---|
| تتبع عند الحصول | قراءة الخاصية → تسجيل "من يستخدمها" |
| عند تعيين مشغل | تغيير الخاصية → إخطار "جميع المستخدمين" |
| تخزين WeakMap | التخلص التلقائي من الذاكرة (يتم مسحها تلقائيًا عند تدمير الكائن المستهدف) |
| activeEffect | التأثير الذي يتم تنفيذه حاليًا (render / watch / computed) |
| المعالجة المجمعة | تؤدي التعديلات المتعددة إلى تحديث واحد فقط (nextTick) |
5. التنفيذ الداخلي لـ ref
(1) شفرة المصدر لـ ref (النسخة المبسطة)
// Vue 3 ref Internal Implementation
class RefImpl {
constructor(value) {
this._value = isObject(value) ? reactive(value) : value
}
get value() {
// 1. track
track(this, 'value')
return this._value
}
set value(newValue) {
// 1. Compare the old and new values
if (hasChanged(this._value, newValue)) {
this._value = isObject(newValue) ? reactive(newValue) : newValue
// 2. trigger
trigger(this, 'value')
}
}
}
function ref(value) {
return new RefImpl(value)
}
(2) كيف تعمل عملية فك الضغط التلقائي
<template>
<!-- Automatic unpacking in templates:{{ count }} rather than {{ count.value }} -->
<p>{{ count }}</p>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0(JS Domestic Demand .value)
console.log(count) // 0(devtools Displayed in the middle 0)
</script>
المبدأ: أثناء ترجمة القالب، يقوم المُترجم تلقائيًا بإضافة .value إلى ref (لا داعي لكتابته يدويًّا).
6. التنفيذ الداخلي لـ computed
(1) tag «كسول + قذر»
// Vue 3 computed Simplified Implementation
function computed(getter) {
const ref = {
// 1. Cache value
_value: undefined,
// 2. Has it expired?(Needs to be recalculated)
_dirty: true,
// 3. getter
get value() {
// Lazy Evaluation:Counted only on the first visit
if (this._dirty) {
this._value = effect(getter) // Collect Dependencies
this._dirty = false
}
// 4. track(Who is using me?)
track(this, 'value')
return this._value
}
}
// 5. When dependencies change,Mark dirty
effect(() => getter()).scheduled = () => {
ref._dirty = true
trigger(ref, 'value') // For triggering computed Component Updates
}
return ref
}
(2) آلية التخزين المؤقت
const count = ref(0)
const double = computed(() => count.value * 2)
console.log(double.value) // 0(First Calculation)
console.log(double.value) // 0 (Cached, Do not recalculate)
count.value = 5
console.log(double.value) // 10(Recalculation)
7. تحسين الأداء: shallowRef / shallowReactive / markRaw
(1) shallowRef: استجابة سطحية
import { shallowRef, triggerRef } from 'vue'
const products = shallowRef([
{ id: 1, name: 'iPhone', price: 999 },
{ id: 2, name: 'MacBook', price: 2499 }
])
// ❌ Not responding(Edit .value Internal Properties)
products.value[0].name = 'iPad'
// Will not trigger an update(shallowRef Follow Only .value Replace All)
// ✅ Trigger an update(Replace All)
products.value = [...products.value, { id: 3, name: 'iPad' }]
// ✅ Force Trigger(Manually after making internal changes trigger)
products.value[0].name = 'iPad'
triggerRef(products)
(2) shallowReactive: تفاعلية سطحية
import { shallowReactive } from 'vue'
const state = shallowReactive({
user: { name: 'Alice' },
settings: { theme: 'dark' }
})
// ✅ Response:Changes to Top-Level Properties
state.user = { name: 'Bob' }
// ❌ Not responding:Nested Properties
state.user.name = 'Charlie'
(3) markRaw: لا يستجيب أبدًا
import { markRaw, reactive } from 'vue'
// Examples of Third-Party Libraries (e.g. ECharts, Mapbox) No need for reactive design
const map = markRaw(new Map())
const chart = markRaw(echarts.init(element))
const state = reactive({
map, // ✅ Will not be Proxy Packaging
chart // ✅ Performance Improvements 30%
})
(4) 5 حالات استخدام رئيسية
| السيناريو | الحل | تحسين الأداء |
|---|---|---|
| قائمة البيانات الضخمة (100,000+) | shallowRef | 50x |
| أمثلة على المكتبات الخارجية | markRaw | 30% |
| إنشاء/إزالة الكائنات بشكل متكرر | shallowReactive | 20% |
| التكوين للقراءة فقط | markRaw | 10% |
| البيانات الثابتة | shallowRef | 5% |
8. مثال كامل: تطبيق مبادئ التصميم المتجاوب
▶ مثال: 1. نسخة مبسطة من «المسار + المشغل»
// Simulation Vue 3 Responsive Systems
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
if (activeEffect) {
dep.add(activeEffect)
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) dep.forEach(effect => effect())
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return Reflect.get(target, key)
},
set(target, key, value) {
const result = Reflect.set(target, key, value)
trigger(target, key)
return result
}
})
}
// Usage
const state = reactive({ count: 0 })
let render = null
// Simulation render Function
activeEffect = () => console.log(`Render: count = ${state.count}`)
render = activeEffect
render() // Output "Render: count = 0"
state.count = 1 // Trigger render,Output "Render: count = 1"
▶ مثال: 2. مقارنة بين مؤشرات الأداء الخمسة الرئيسية
| الوضع | الأداء عند 100,000 سطر | الذاكرة | قابل للتطبيق |
|---|---|---|---|
ref([]) |
100 مللي ثانية | 100 ميغابايت | بيانات صغيرة |
reactive({...}) |
100 مللي ثانية | 100 ميغابايت | كائنات متداخلة |
shallowRef([]) |
5 مللي ثانية | 5 ميغابايت | قائمة كبيرة |
shallowReactive({...}) |
30 مللي ثانية | 30 ميغابايت | كائن سطحي |
markRaw({...}) |
2 مللي ثانية | 2 ميغابايت | مثيل تابع لجهة خارجية |
▶ مثال: 3. البنية الداخلية لـ ref + reactive
import { ref, reactive, shallowRef, markRaw } from 'vue'
// ref:Wrap any value
const count = ref(0)
console.log(count) // RefImpl { _value: 0, __v_isRef: true, ... }
// reactive:Items to Be Packaged
const state = reactive({ count: 0 })
console.log(state) // Proxy { count: 0, ... }
// shallowRef: Shallow
const list = shallowRef([1, 2, 3])
console.log(list) // RefImpl { _value: [1, 2, 3] } // Arrays are not reactive Packaging
// markRaw:Does not respond at all
const map = markRaw(new Map())
console.log(map) // Map { ... } // Regular Map,No Proxy
▶ مثال: 4. خمسة أسباب جذرية شائعة للفشل
| سيناريو الفشل | السبب الجذري | الحل |
|---|---|---|
| استبدال الكائن التفاعلي بأكمله | لقد تغيرت الإشارة التفاعلية | قم بتغليفها في متغير «ref» أو تعديل خصائصها |
| تحليل الاستجابة المفقودة | تحليل القيمة الأصلية | استخدام toRefs |
| تغيير مصفوفة shallowRef لا يُطلق التحديث | shallowRef يتتبع فقط استبدال .value | استبدل المصفوفة أو استخدم triggerRef |
| المهمة غير المتزامنة لا تستجيب | الدالة غير المتزامنة خارج المسار | التغليف باستخدام ref/reactive |
| لا يتم تحديث خصائص الكائنات المتداخلة | تم إصلاح المشكلة في Vue 3 (يتطلب Vue 2 استخدام Vue.set) | التحديثات التلقائية |
▶ مثال: 5. معيار أداء الخادم الوكيل
// Performance Testing:10,000 A property
const obj = {}
for (let i = 0; i < 10000; i++) {
obj[`key${i}`] = i
}
console.time('Object.defineProperty')
// ... 10,000 getter/setter
console.timeEnd('Object.defineProperty')
// Usually 50-100ms
console.time('Proxy')
const proxy = new Proxy(obj, { /* ... */ })
console.timeEnd('Proxy')
// Usually 1-5ms(Create only when accessed)
▶ مثال: 6. 5 نصائح أساسية للتصحيح
// 1. watchEffect Printing Dependencies
watchEffect(() => {
console.log('Dependency:', { a: state.a, b: state.b })
})
// 2. Trigger a manual update(forceUpdate)
import { triggerRef } from 'vue'
triggerRef(stateRef)
// 3. Check the responsive status
import { isReactive, isRef, isReadonly, isProxy } from 'vue'
isReactive(obj) // true/false
isRef(refObj) // true/false
// 4. Original Object(Remove Proxy)
import { toRaw } from 'vue'
const raw = toRaw(reactiveObj)
// 5. Performance Labels
import { markRaw } from 'vue'
const fast = markRaw(expensiveObj)
❓ أسئلة شائعة
track وtrigger؟track في الأسلوب get لكائن تفاعلي (لتسجيل التبعيات عند استدعاء get). يتم استدعاء trigger في الطريقة set (لإخطار جميع التبعيات عند استدعاء set).shallowRef وref؟ref هو استجابة عميقة (الكائنات المتداخلة هي أيضًا وكلاء). أما shallowRef فيقتصر على تتبع استبدال .value بالكامل ولا يتعامل مع الخصائص الداخلية. وهو مناسب للقوائم الكبيرة (100,000+ صف).computed متى تتغير إحدى التبعيات؟computed داخليًّا بتغليف دالة الاسترجاع (getter) في effect. وعندما تتغير البيانات التفاعلية المستخدمة في دالة الاسترجاع، يتم تشغيل effect، مما يؤدي إلى إعادة تعيين tag dirty بحيث يتم إعادة حساب القيمة عند الوصول إليها في المرة التالية.watch وwatchEffect؟watch تحدد المصدر (التبعية) بشكل صريح، وتستقبل دالة الاستدعاء كلًا من newVal وoldVal. أما watchEffect فلا يحدد مصدرًا؛ فهو يتتبع التبعيات تلقائيًا داخل جسم الدالة، ولا يتلقى دالة الاستدعاء إلا القيمة الجديدة.shallowRef؟triggerRef(shallowRef). السيناريو: فرض تحديث العرض بعد إجراء تعديل جماعي على الخصائص الداخلية.reactive في وحدة التحكم؟Proxy { ... } wrapper object; the properties of the original object are not visible. Use toRaw(proxyObj) لاسترداد الكائن الأصلي.📖 ملخص
- النواة التفاعلية في Vue 3: اعتراض البروكسي + track (جمع التبعيات) + trigger (تشغيل التحديثات)
- Proxy أقوى من
Object.defineProperty: فهو يدعم المصفوفات و«Maps» و«Sets» بشكل أصلي، ويستجيب تلقائيًا للخصائص الجديدة. - مرجع داخلي: فئة RefImpl؛ التتبع/التشغيل أثناء عمليات الاسترجاع/التعيين
- داخل
computed: tag "dirty" + التقييم المتأخر + التخزين المؤقت - 5 تحسينات للأداء: shallowRef / shallowReactive / markRaw
- 5 أسباب شائعة لفشل الاستجابة: الاستبدال / تفكيك الهياكل / المصفوفات / العمليات غير المتزامنة / التداخل
📝 تمارين
-
أسئلة أساسية (مستوى الصعوبة: ⭐)
تنفيذ نسخة مبسطة من نظام تفاعلي (يعتمد على البروكسي):
- دالة تفاعلية مكونة من 5 أسطر
- 5 خطوط تتبع + مشغل
- العرض التوضيحي: الضبط 100 مرة → تشغيل التأثير مرة واحدة (المعالجة المجمعة)
-
مسائل متقدمة (مستوى الصعوبة: ⭐⭐)
تحسين القوائم الكبيرة باستخدام
shallowRefوmarkRaw:- قائمة تضم 1,000 منتج (باستخدام shallowRef)
- يتضمن مثيلات لمكتبات تابعة لجهات خارجية (باستخدام
markRaw) - قياس استخدام الذاكرة والتحسينات في معدل الإطارات في الثانية (FPS)
-
مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)
تنفيذ «أداة تتبع التبعيات» كاملة:
- تطبع الدالة watchEffect جميع التبعيات
- triggerRef: تشغيل التحديث يدويًّا
- شرح وظائف toRaw و isReactive و isRef المساعدة
- تحليل الأسباب الجذرية لخمسة سيناريوهات للفشل (استبدال المرجع / التفكيك / المصفوفات / عدم التزامن / التداخل)
- معايير الأداء: مقارنة الذاكرة بين ref و shallowRef و markRaw