Skip to content

ناظرها - Watchers

مثال پایه

ویژگی‌های computed به شما این امکان را می‌دهند که مقادیر جدید را براساس داده‌های موجود محاسبه کنید. با این حال در یک سری از موارد ما باید در واکنش به تغییرات، عملیات‌هایی را انجام دهیم. برای مثال تغییر در DOM یا تغییر بخشی از state که نتیجه یک عملیات همگام است.

در Options API، ما می‌توانیم از آپشن watch استفاده کنیم تا هروقت یک reactive تغییر کرد، یک تابع را فراخوانی کند:

js
export default {
  data() {
    return {
      question: '',
      answer: '.سوالی که مینویسید باید شامل علامت سوال (؟) باشد',
      loading: false
    }
  },
  watch: {
    // تغییر کند، این تابع اجرا خواهد شد question هر زمان که
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('؟')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = '...درحال فکر کردن'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = '.خطا! دسترسی به لینک ممکن نیست ' + error
      } finally {
        this.loading = false
      }
    }
  }
}
template
<p>
  یک سوال بله/خیر بپرسید:
  <input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>

آن را در Playground امتحان کنید

آپشن watch از پراپرتی های تو در تو یک آبجکت هم پشتیبانی می‌کند.

js
export default {
  watch: {
    // توجه: عبارات پشتیبانی نمی‌شوند و فقط می‌توان از پراپرتی‌های آبجکت استفاده کرد
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

در 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 به طور پیش‌فرض فقط زمانی فعال می‌شود که مقدار جدید به ویژگی اختصاص داده شود و در تغییرات تودرتو پراپرتی فعال نمی‌شود. اگر می‌خواهید همه تغییرات داخلی پراپرتی را نظارت کنید، باید از نظارت عمیق (deep watcher) استفاده کنید.

js
export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // توجه: در میوتیشن‌های تو در تو 
        // مقدار جدید با مقدار قدیم برابر خواهدبود
        // در صورتی که خود آبجکت جایگزین نشده باشد
      },
      deep: true
    }
  }
}

وقتی 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 بلافاصله اجرا شود - به عنوان مثال، ممکن است بخواهیم ابتدا داده‌های اولیه را دریافت کنیم و سپس هر زمان که وضعیت مرتبط تغییر کرد، داده‌ها را دوباره دریافت کنیم.

ما می‌توانیم با استفاده از تابع handler و آپشن immediate: true، فراخوانی را بلافاصله اجرا کنیم:

js
export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // this will be run immediately on component creation.
      },
      // force eager callback execution
      immediate: true
    }
  }
  // ...
}

اجرای اولیه تابع (handler) به طور دقیق قبل از هوک created اتفاق می‌افتد. Vue در حالت ابتدایی گزینه‌های data computed و methods را پردازش کرده است، بنابراین این ویژگی‌ها در اولین فراخوانی در دسترس خواهند بود.

ما می‌توانیم با استفاده از گزینه 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
export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // تغییر می کند، فقط یک بار فعال می‌شود `source` هنگامی که
      },
      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
  })
})
js
export default {
  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()
  })
})
js
import { onWatcherCleanup } from 'vue'

export default {
  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
  })
})
js
export default {
  watch: {
    id(newId, oldId, 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
export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'post'
    }
  }
}
js
watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

همچنین Post-flush، یک نام مختصر به نام watchPostEffect()‎ دارد.

js
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* اجرا می‌شود Vue ‌پس از به‌روز‌رسانی‌های */
})

ناظران همگام‌سازی‌شده

همچنین می‌توان پیش از هر به‌روزرسانی تحت مدیریت Vue، یک ناظر ایجاد کرد که به طور همزمان فعال شود:

js
export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'sync'
    }
  }
}
js
watch(source, callback, {
  flush: 'sync'
})

watchEffect(callback, {
  flush: 'sync'
})

watchEffect()‎ همگام‌سازی‌شده نیز دارای یک نام مستعار مفید به نام watchSyncEffect()‎ است:

js
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* اجرا می شود reactive به صورت همزمان با تغییر داده های */
})

با احتیاط استفاده کنید

ناظران همگام‌سازی‌شده دسته‌بندی نشده‌اند و هر بار که یک جهش reactive تشخیص داده می‌شود راه‌اندازی می‌شوند. استفاده از آن‌ها برای نظارت بر مقادیر ساده بولی درست است، اما از استفاده آن‌ها روی منابع داده‌ای که ممکن است به صورت همزمان چندین بار جهش یابند، مانند آرایه‌ها، اجتناب کنید.

this.$watch()‎

همچنین می‌توان با استفاده از روش ‎$watch()‎ ناظر ایجاد کرد:

js
export default {
  created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }
}

این ویژگی زمانی مفید است که می‌خواهید یک ناظر را شرطی یا در پاسخ به تعامل کاربر فعال کنید و در مواقعی که دیگر لازم نیست، عملکرد ناظر را متوقف کنید .

متوقف کردن ناظر

ناظرهایی که با استفاده از آپشن watch یا متد ‎$watch()‎ ایجاد می‌شوند، به طور خودکار وقتی کامپوننتی که آنها به آن تعلق دارند از صفحه وب حذف یا "unmount" می‌شود، متوقف می‌شوند. بنابراین در بیشتر موارد نیازی نیست که خودتان نگران متوقف کردن ناظر باشید.

در موارد نادری که می‌خواهید یک ناظر را قبل از حذف کامپوننتی که به آن تعلق دارد متوقف کنید، Vue.js راهی برای انجام این کار با استفاده از متد ‎$watch()‎ارائه می‌کند. این روش یک تابع ویژه را برمی‌گرداند که می‌توانید با فراخوانی آن به صورت دستی ناظر را متوقف کنید:

js
const unwatch = this.$watch('foo', callback)

// زمانی که دیگر به ناظر نیازی نیست:
unwatch()

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) {
    // هنگامی که داده‌ها لود می‌شوند کاری انجام دهید
  }
})
ناظرها - Watchers has loaded