ناظرها - Watchers
مثال پایه
ویژگیهای computed به شما این امکان را میدهند که مقادیر جدید را براساس دادههای موجود محاسبه کنید. با این حال در یک سری از موارد ما باید در واکنش به تغییرات، عملیاتهایی را انجام دهیم. برای مثال تغییر در DOM یا تغییر بخشی از state که نتیجه یک عملیات همگام است.
در Composition API، میتوانیم از تابع watch
استفاده کنیم تا هر زمان یک قسمت از reactive تغییر کرد، یک تابع (callback) را فراخوانی کنیم:
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('.سوالی که مینویسید باید شامل علامت سوال (؟) باشد')
const loading = ref(false)
// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = '...درحال فکر کردن'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = '.خطا! دسترسی به لینک ممکن نیست ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
یک سوال بله/خیر بپرسید:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
آن را در Playground امتحان کنید
تایپهای منابع نظارتی
اولین آرگومان watch
میتواند تایپهای مختلفی از «منابع» reactive باشد: میتواند یک ref (شامل computed refs)، یک reactive object، یک getter function یا آرایه باشد:
js
const x = ref(0)
const y = ref(0)
// single ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter تابع
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// آرایه
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
توجه داشته باشید که نمیتوانید یک reactive object را مانند مثال زیر نظارت کنید:
js
const obj = reactive({ count: 0 })
// پاس میدهیم watch() را به number کار نمیکند زیرا داریم یک
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})
در عوض، از یک getter استفاده کنید:
js
// instead, use a getter:
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
ناظران عمیق
وقتی watch()
را مستقیماً روی یک آبجکت reactive فراخوانی میکنید، به طور خودکار یک نظارت عمیق ایجاد میشود. این به این معناست که تابع callback به صورت خودکار برای تمام تغییرات در تمام بخشهای درونی این آبجکت اجرا میشود.
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// این رویداد در صورت تغییرات در پراپرتیهای تو در تو اتفاق میافتد
// توجه: مقدار جدید در اینجا برابر با مقدار قدیمی خواهدبود
// !زیرا هر دو به همان آبجکت اشاره میکنند
})
obj.count++
این مورد باید از یک getter که یک آبجکت reactive را برمیگرداند متمایز شود - در مورد دوم، تابع callback تنها در صورتی فعال میشود که getter یک آبجکت متفاوت را برگرداند:
js
watch(
() => state.someObject,
() => {
// جایگزین شود state.someObject فقط زمانی فعال میشود که
}
)
میتوانید با استفاده از گزینه deep
مورد دوم را به نظارت عمیق تبدیل کنید:
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Note: `newValue` will be equal to `oldValue` here
// *unless* state.someObject has been replaced
},
{ deep: true }
)
در Vue 3.5+، آپشن deep
میتواند عددی باشد که حداکثر عمق پیمایش را نشان میدهد - یعنی Vue چند سطح باید از ویژگیهای تودرتوی یک آبجکت عبور کند.
احتیاط کنید
نظارت عمیق نیازمند بررسی تمام ویژگیهای تودرتو در آبجکت مشاهده شده است و ممکن است زمانبر باشد، به ویژه زمانی که بر روی ساختارهای داده بزرگ استفاده میشود. از آن تنها زمانی استفاده کنید که واقعاً ضروری باشد و مراقب پیامدهای عملکردی آن باشید.
ناظران آنی
نظارت watch
به طور پیشفرض بلافاصله نیست، به این معنا که تابع callback تا زمانی که پراپرتی نظارت شده تغییر نکند، فراخوانی نمیشود. اما در برخی موارد، ممکن است بخواهیم تابع callback بلافاصله اجرا شود - به عنوان مثال، ممکن است بخواهیم ابتدا دادههای اولیه را دریافت کنیم و سپس هر زمان که وضعیت مرتبط تغییر کرد، دادهها را دوباره دریافت کنیم.
ما میتوانیم با استفاده از گزینه immediate: true
، فراخوانی را بلافاصله اجرا کنیم:
js
watch(
source,
(newValue, oldValue) => {
// executed immediately, then again when `source` changes
},
{ immediate: true }
)
Once Watchers
- Only supported in 3.4+
هر زمانی که منبع داده ناظر تغییر کند ناظر کالبک خود را فراخوانی میکند. اگر میخواهید کالبک ناظر فقط یک بار در زمان تغییر داده صدا زده شود، از آپشن once: true
استفاده کنید.
js
watch(
source,
(newValue, oldValue) => {
// تغییر می کند، فقط یک بار فعال میشود `source` هنگامی که
},
{ 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
دو بار استفاده میکند، یک بار به عنوان ابتدا به عنوان منبع تشخیص تغییرات و سپس دوباره در داخل callback.
این کد را میتوان با watchEffect()
ساده تر کرد. watchEffect()
فرآیند نظارت بر تغییرات در دادههای شما را با تشخیص خودکار آنچه کد شما به آن وابسته است، ساده میکند و هر زمان که وابستگیها تغییر میکنند، callback اجرا میشود. ناظر بالا را میتوان به صورت زیر بازنویسی کرد:
js
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
در اینجا، تابع callback بلافاصله اجرا میشود و نیازی به مشخص کردن immediate: true
نیست. در طی اجرای آن، به طور خودکار todoId.value
را به عنوان یک وابستگی دنبال میکند (مشابه ویژگیهای computed). هر زمان که todoId.value
تغییر کند، تابع callback مجدداً اجرا میشود. با استفاده از watchEffect()
، دیگر نیازی نیست todoId
را به عنوان مقدار ارسال کنیم.
شما میتوانید این مثال را برای دیدن نمونهای از استفاده از watchEffect()
بررسی کنید. این مثال به شما نشان میدهد که چگونه میتوانید تغییرات در دادهها را نظارت کرده و به آنها واکنش نشان دهید.
برای ناظرهایی که دارای چندین وابستگی هستند، استفاده از watchEffect()
مسئولیت دستی مدیریت لیست وابستگیها را برطرف میکند. علاوه بر این، اگر نیاز به نظارت چندین پراپرتی در یک ساختار داده تو در تو داشته باشید، watchEffect()
ممکن است به نسبت یک ناظر عمیق (deep watcher) موثرتر باشد، زیرا تنها پراپرتیهایی که در تابع callback استفاده میشوند را پیگیری میکند و به جای پیگیری همگی آنها به صورت بازگشتی، بهینهتر عمل میکند.
نکته
watchEffect
تنها در حین اجرای همگام(synchronous) خود وابستگیها را دنبال میکند. هنگام استفاده از آن با یک تابع ناهمگام(async)، تنها پراپرتیهایی که قبل از اولین دستور await
مشخص شده باشند، دنبال میشوند.
watch
در مقابل watchEffect
watch
و watchEffect
هر دو به ما اجازه میدهند که عملیاتهایی را در پاسخ به تغییرات انجام دهیم. تفاوت اصلی بین آنها در نحوه پیگیری وابستگیهاست.
watch
فقط منبع مشخصی را دنبال میکند و تغییرات داخل تابع callback را نادیده میگیرد. علاوه بر این، تابع تنها زمانی فعال میشود که منبع واقعاً تغییر کرده باشد.watch
به شما امکان میدهد مشخص کنید چه دادهها یا منابعی را میخواهید دنبال کنید (ردیابی وابستگی)، و زمانی که آن دادهها یا ویژگیها تغییر میکنند چه اتفاقی بیفتد (اثر جانبی). این تفکیک، باعث میشود کنترل دقیقتری در مورد زمانی که تابع باید اجرا شود، داشته باشیم.watchEffect
از سوی دیگر، ردیابی وابستگی و اجرای اثر جانبی را در یک مرحله ترکیب میکند. و به طور خودکار هر پراپرتی را که کد شما به آن دسترسی دارد را در حالی که به طور همزمان اجرا میشود، ردیابی میکند. به عبارت سادهتر، وقتی از watchEffect استفاده میکنید، به تمام داده ها یا ویژگیهایی که کد شما در داخل بلوک کد خود با آنها تعامل دارد توجه میکند. این روش به کدنویسی بهتر و سادهتر منجر میشود، اما وابستگیهای آن کمتر مشخص اند.
پاکسازی اثرات جانبی
گاهی اوقات ممکن است اثرات جانبی داشته باشیم، به عنوان مثال. درخواستهای ناهمزمان، در یک ناظر:
js
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// callback logic
})
})
اما اگر id
قبل از تکمیل درخواست تغییر کند چه؟ هنگامی که درخواست قبلی تکمیل شد، باز هم پاسخ تماس را با یک مقدار آیدی که قبلاً فرستاده شده است ارسال می کند. در حالت ایدهآل، میخواهیم زمانی که id
به یک مقدار جدید تغییر میکند، بتوانیم درخواست قدیمی را لغو کنیم.
ما میتوانیم از onWatcherCleanup()
API برای ثبت یک تابع پاکسازی استفاده کنیم که وقتی ناظر نامعتبر شد و در شرف اجرای مجدد است فراخوانی میشود:
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()
})
})
توجه داشته باشید که onWatcherCleanup
فقط در Vue 3.5+ پشتیبانی میشود و باید در حین اجرای همزمان یک تابع افکت watchEffect
یا تابع callback برای watch
فراخوانی شود: نمیتوانید آن را پس از یک عبارت await
در یک تابع async فراخوانی کنید.
از طرف دیگر، یک تابع onCleanup
نیز به عنوان آرگومان سوم به تابع کالبک watch و به عنوان اولین آرگومان به تابع watchEffect
ارسال می شود:
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
این در نسخه های قبل از 3.5 کار می کند. علاوه بر این، onCleanup
که از طریق آرگومان تابع ارسال میشود به نمونه تماشاگر محدود میشود، بنابراین تحت محدودیت همزمان onWatcherCleanup
قرار نمیگیرد.
زمانبندی اجرای callback ها
وقتی شما reactive state را تغییر میدهید، این ممکن است باعث فراخوانی همزمان بهروزرسانی کامپوننت های Vue و ناظرهای ایجاد شده توسط شما شود.
مشابه بهروزرسانی کامپوننتها، توابع watcher ایجاد شده توسط کاربر به صورت دستهای فراخوانی میشوند تا از فراخوانیهای تکراری جلوگیری کند. به عنوان مثال، احتمالاً نمیخواهیم watcher هزار بار فراخوانی شود اگر همزمان هزار آیتم را به یک آرایهای که تحت نظارت است اضافه کنیم.
بهطور پیشفرض، تابع callback یک watcher بعد از بهروزرسانی کامپوننت والد (اگر وجود داشته باشد) و قبل از بهروزرسانی DOM کامپوننت مالک فراخوانی میشود. این بدان معناست اگر سعی کنید در داخل تابع callback یک watcher به DOM خود کامپوننت مالک دسترسی پیدا کنید، DOM در حالت قبل از بهروزرسانی خواهد بود.
Post Watchers
برای دسترسی به DOM کامپوننت مالک در تابع callback یک تماشاگر بعد از آنکه Vue آن را به روز کرده است، باید آپشن flush: 'post'
را فعال کنید:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
همچنین Post-flush، یک نام مختصر به نام watchPostEffect()
دارد.
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* اجرا میشود Vue پس از بهروزرسانیهای */
})
ناظران همگامسازیشده
همچنین میتوان پیش از هر بهروزرسانی تحت مدیریت Vue، یک ناظر ایجاد کرد که به طور همزمان فعال شود:
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
watchEffect()
همگامسازیشده نیز دارای یک نام مستعار مفید به نام watchSyncEffect()
است:
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* اجرا می شود reactive به صورت همزمان با تغییر داده های */
})
با احتیاط استفاده کنید
ناظران همگامسازیشده دستهبندی نشدهاند و هر بار که یک جهش reactive تشخیص داده میشود راهاندازی میشوند. استفاده از آنها برای نظارت بر مقادیر ساده بولی درست است، اما از استفاده آنها روی منابع دادهای که ممکن است به صورت همزمان چندین بار جهش یابند، مانند آرایهها، اجتناب کنید.
متوقف کردن ناظر
watcher هایی که به صورت همزمان در داخل setup()
یا <script setup>
تعریف میشوند، به کامپوننت والد متصل می شوند و هنگامی که کامپوننت والد حذف شود، به طور خودکار متوقف میشوند. بنابراین در بیشتر موارد نیازی نیست که خودتان نگران متوقف کردن ناظر باشید.
نکته کلیدی در اینجا این است که ناظر باید همزمان ایجاد شود: اگر ناظر در یک فراخوانی ناهمگام ایجاد شود، بعد از حذف کامپوننت والد متوقف نمیشود و باید به صورت دستی متوقف شود تا از نشت حافظه جلوگیری کند. مثال زیر را ببینید:
vue
<script setup>
import { watchEffect } from 'vue'
// این یکی به طور خودکار متوقف خواهد شد
watchEffect(() => {})
// ... این یکی خودکار متوقف نمیشود!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
برای متوقف کردن یک ناظر به صورت دستی باید از تابع unwatch()
استفاده کنید. این کار برای هر دو watch
و watchEffect
کار میکند.
js
const unwatch = watchEffect(() => {})
// ... بعداً، زمانی که دیگر مورد نیاز نیست
unwatch()
توجه داشته باشید که موارد کمی وجود دارند که نیاز به ایجاد نظارت ناهمگام دارید و بهتر است اگر امکان دارد از نظارت همزمان استفاده کنید. اگر باید منتظر برخی از دادههای ناهمگام باشید، به جای آن میتوانید منطق ناظر خود را برعکس کنید:
js
// دادههایی که به صورت ناهمزمان بارگذاری میشوند
const data = ref(null)
watchEffect(() => {
if (data.value) {
// هنگامی که دادهها لود میشوند کاری انجام دهید
}
})