الخاصيات المُراقبة
مثال أساسي
الخاصيات المحسوبة تسمح لنا بحساب القيم المشتقة بشكل تصريحي. ومع ذلك، هناك حالات حيث نحتاج إلى تنفيذ "تأثيرات جانبية" بالرد على تغييرات الحالة - على سبيل المثال، تعديل DOM، أو تغيير جزء آخر من الحالة بناءً على نتيجة عملية غير متزامنة.
مع الواجهة التركيبية، يمكننا استخدام الدالة watch
لتشغيل دالة عند تغيير حالة تفاعلية :
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('عادة ما تحتوي الأسئلة على علامة استفهام. ;-)')
watch(question, async (newQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = 'قيد التفكير ...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'خطأ! تعذر الوصول إلى الخادم. ' + error
}
}
})
</script>
<template>
<p>
اطرح سؤال نعم/لا:
<input v-model="question" />
</p>
<p>{{ answer==='yes'?'نعم':'لا' }}</p>
</template>
أنواع مصادر الدالة المُراقبة
watch
's first argument can be different types of reactive "sources": it can be a ref (including computed refs), a reactive object, a getter function, or an array of multiple sources:
js
const x = ref(0)
const y = ref(0)
// ref وحيد
watch(x, (newX) => {
console.log(`x يساوي ${newX}`)
})
// دالة مُحصِّلة
watch(
() => x.value + y.value,
(sum) => {
console.log(`مجموع x + y يساوي: ${sum}`)
}
)
// مصفوفة من مصادر متعددة
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x يساوي ${newX} و y يساوي ${newY}`)
})
تجدر الإشارة إلى أنه لا يمكنك مراقبة خاصية من كائن تفاعلي كهذا المثال :
js
const obj = reactive({ count: 0 })
// لن يشتغل هذا لأننا نمرر رقمًا إلى
// watch()
watch(obj.count, (count) => {
console.log(`العداد: ${count}`)
})
بدلاً من ذلك استعمل دالة مُحصِّلة لارجاع الخاصية:
js
// بدلاً من ذلك استعمل دالة مُحصِّلة لارجاع الخاصية:
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
الخاصيات المُراقبة العميقة
عندما تستدعي ()watch
مباشرةً على كائن تفاعلي، سيُنشئ تلقائيًا دالة مُراقبة عميقة - ستُشغَّل دالة رد النداء على جميع التغييرات المتداخلة :
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// تشغل عند التغييرات المتداخلة للخاصية ملاحظة:
// `newValue` سيكون مساويًا لـ `oldValue` هنا
// لأنهما يشيران إلى نفس الكائن!
})
obj.count++
This should be differentiated with a getter that returns a reactive object - in the latter case, the callback will only fire if the getter returns a different object:
js
watch(
() => state.someObject,
() => {
// تشغل فقط عند استبدال state.someObject
}
)
ومع ذلك، يمكنك فرض الحالة الثانية داخل دالة مراقبة عميقة عن طريق استخدام الخيار "deep" بشكل صريح :
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// ملاحظة: `newValue` سيكون مساويًا لـ `oldValue` هنا
// *إلا إذا* تم استبدال state.someObject
},
{ deep: true }
)
In Vue 3.5+, the deep
option can also be a number indicating the max traversal depth - i.e. how many levels should Vue traverse an object's nested properties.
Use with Caution
Deep watch requires traversing all nested properties in the watched object, and can be expensive when used on large data structures. Use it only when necessary and beware of the performance implications.
الدوال المُراقِبة الفورية
الدالة watch
خاملة افتراضيًا: لن تُستدعى دالة رد النداء حتى يُغيَّر المصدر المراقَب. ولكن في بعض الحالات قد نرغب في تشغيل نفس شيفرة رد النداء بشكل فوري - على سبيل المثال، قد نرغب في جلب بعض البيانات الأولية ثم إعادة جلبها عند تغيير الحالة المرتبطة بها.
يمكننا فرض تشغيل دالة رد النداء للخاصية المُراقبة فورًا عن طريق التصريح بها باستخدام كائن يحتوي على دالة المعالجة handler
لتنفيذ الشيفرة المستندة على تغير المصدر المُراقَب وخيار immediate: true
:
js
watch(source, (newValue, oldValue) => {
// سيتم تشغيله فورًا ثم مرة أخرى عند تغيير المصدر `source`
}, { immediate: true })
Once Watchers
- Only supported in 3.4+
Watcher's callback will execute whenever the watched source changes. If you want the callback to trigger only once when the source changes, use the once: true
option.
js
watch(
source,
(newValue, oldValue) => {
// when `source` changes, triggers only once
},
{ once: true }
)
()watchEffect
من الشائع أن تستخدم دالة رد النداء نفس الحالة المرتبطة بالمصدر المُراقَب. على سبيل المثال، فإن الشيفرة التالية تستخدم مُراقِب لتحميل مورد عن بعد عند تعيين المتغير التفاعلي todoId
:
js
const todoId = ref(1)
const data = ref(null)
watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)
على وجه الخصوص، تجدر الإشارة إلى أن الدالة المُراقِبة تستخدم todoId
مرتين، مرة كمصدر ومرة أخرى داخل الدالة المُعالجة.
يمكن تبسيط هذا باستخدام ()watchEffect
. يسمح لنا ()watchEffect
بتتبع الاعتماديات التفاعلية للدالة المعالجة تلقائيًا. يمكن إعادة كتابة المُراقِب أعلاه كما يلي:
js
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
هنا، ستُشغَّل الدالة المُعالِجة فورًا، لا تحتاج إلى تحديد immediate: true
. خلال تنفيذها، سيتم تتبع todoId.value
تلقائيًا كاعتمادية (مماثلة للخاصيات المحسوبة). عندما يتغير todoId.value
، سيتم تشغيل الدالة المعالجة مرة أخرى. مع ()watchEffect
، لم نعد نحتاج إلى تمرير todoId
بشكل صريح كقيمة مصدرية.
يمكنك التحقق من هذا المثال لـ ()watchEffect
و كيفية تحميل البيانات التفاعلية داخل دالتها المعالجة.
بالنسبة للأمثلة مثل هذه، مع اعتمادية واحدة فقط، فإن فائدة ()watchEffect
غير ملموسة نسبيا. ولكن hلدوال المُراقِبة التي لديها عدة اعتماديات، فإن استخدام ()watchEffect
يزيل قائمة الاعتماديات المضافة يدويًا. بالإضافة إلى ذلك، إذا كنت بحاجة إلى مراقبة عدة خاصيات في سلسلة بيانات متداخلة، فقد تثبت الدالة ()watchEffect
فعاليتها مقارنة بالدالة المُراقِبة العميقة، حيث سيتم تتبع الخاصيات المستخدمة فقط في الدالة المعالجة، بدلاً من تتبعها جميعا بشكل تكراري .
ملاحظة
()watchEffect
يتتبع الاعتماديات فقط خلال تنفيذها المتزامن. عند استخدامه مع دالة مُعالِجة غير متزامنة، وحدها الخاصيات التي يتم الوصول إليها قبل النبضة الأولى لـawait
ستُتبَّع .
watch
مقابل watchEffect
watch
و watchEffect
كلتاهما تسمحان لنا بتنفيذ تأثير جانبية بشكل تفاعلي. الفرق الرئيسي بينهما هو طريقة تتبع اعتمادياتهما التفاعلية :
watch
تتبع فقط المصدر المراقب بشكل صريح. لن يتم تتبع أي شيء يتم الوصول إليه داخل الدالة المُعالجة. بالإضافة إلى ذلك، فإن الدالة المعالجة تتفاعل فقط عندما يتغير المصدر بالفعل.watch
تفصل التتبع الاعتمادي عن التأثير الجانبي، مما يمنحنا مزيدًا من التحكم الدقيق في متى يجب تشغيل الدالة المعالجة.watchEffect
, من ناحية أخرى، تجمع التتبع الاعتمادي والتأثير الجانبي في طور واحد. تتبع كل الخاصيات التفاعلية التي يتم الوصول إليها خلال تنفيذها بشكل متزامن.هذه الدالة تعتبر أكثر ملاءمة وعادة ما يؤدي ذلك إلى تقليل الشيفرة، ولكنه يجعل اعتمادياته التفاعلية أقل وضوحًا.
Side Effect Cleanup
Sometimes we may perform side effects, e.g. asynchronous requests, in a watcher:
js
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// callback logic
})
})
But what if id
changes before the request completes? When the previous request completes, it will still fire the callback with an ID value that is already stale. Ideally, we want to be able to cancel the stale request when id
changes to a new value.
We can use the onWatcherCleanup()
API to register a cleanup function that will be called when the watcher is invalidated and is about to re-run:
js
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// callback logic
})
onWatcherCleanup(() => {
// abort stale request
controller.abort()
})
})
Note that onWatcherCleanup
is only supported in Vue 3.5+ and must be called during the synchronous execution of a watchEffect
effect function or watch
callback function: you cannot call it after an await
statement in an async function.
Alternatively, an onCleanup
function is also passed to watcher callbacks as the 3rd argument, and to the watchEffect
effect function as the first argument:
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
This works in versions before 3.5. In addition, onCleanup
passed via function argument is bound to the watcher instance so it is not subject to the synchronously constraint of onWatcherCleanup
.
توقيت تنفيذ الدالة المعالجة
لما تُعدل حالة تفاعلية، فإنها قد تُشغل كل من تحديثات مكونات Vue والدوال المراقِبة المُنشأة من قبلك.
Similar to component updates, user-created watcher callbacks are batched to avoid duplicate invocations. For example, we probably don't want a watcher to fire a thousand times if we synchronously push a thousand items into an array being watched.
By default, a watcher's callback is called after parent component updates (if any), and before the owner component's DOM updates. This means if you attempt to access the owner component's own DOM inside a watcher callback, the DOM will be in a pre-update state.
Post Watchers
If you want to access the owner component's DOM in a watcher callback after Vue has updated it, you need to specify the flush: 'post'
option:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
تمتلك ()watchEffect
مسمى ملائم أيضًا لخاصية التنفيذ بعد التحديث، وهي الدالة ()watchPostEffect
:
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* نفذت بعد تحديثات Vue */
})
Sync Watchers
It's also possible to create a watcher that fires synchronously, before any Vue-managed updates:
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
Sync watchEffect()
also has a convenience alias, watchSyncEffect()
:
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* executed synchronously upon reactive data change */
})
Use with Caution
Sync watchers do not have batching and triggers every time a reactive mutation is detected. It's ok to use them to watch simple boolean values, but avoid using them on data sources that might be synchronously mutated many times, e.g. arrays.
إيقاف خاصية مراقِبة
الخاصيات المراقِبة المصرحة بشكل تزامني داخل setup()
أو <script setup>
مرتبطة بنسخة المكون الحامل لها، وسيتم إيقافها تلقائيًا عند فصله. في معظم الحالات، لا تهتم بشأن إيقاف الخاصية المراقِبة بنفسك.
الجوهر هنا هو أن الخاصية المراقِبة يجب أن تُنشأ بشكل متزامن : إذا تم إنشاء الخاصية المراقِبة في دالة رد نداء غير متزامنة، فلن تكون مرتبطة بالمكون الحامل لها ويجب إيقافها يدويًا لتجنب التسريبات في الذاكرة. هنا مثال:
vue
<script setup>
import { watchEffect } from 'vue'
// هذا سيوقف تلقائيًا
watchEffect(() => {})
//هذا لن يوقف تلقائيًا!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
من أجل إيقاف الخاصية المراقِبة يدويًا، استخدم الدالة المُرجَعة. هذا يعمل لكل من watch
و watchEffect
:
js
const unwatch = watchEffect(() => {})
// ... في وقت لاحق، عندما لا تعود محتاجا إلى المراقبة
unwatch()
تجدر الإشارة إلى أن هناك حالات نادرة جدًا حيث تحتاج إلى إنشاء مراقِبين بشكل غير متزامن، ويجب تفضيل الإنشاء المتزامن في أي وقت ممكن. إذا كنت بحاجة إلى الانتظار لبعض البيانات غير المتزامنة، يمكنك جعل شيفرة الدالة المراقبة شرطية بدلاً من ذلك :
js
// بيانات ستُحمّل بشكل غير متزامن
const data = ref(null)
watchEffect(() => {
if (data.value) {
// قم ببعض المعالجة عندما يتم تحميل البيانات
}
})