Skip to content

Watchers

Ví Dụ Cơ Bản

Computed properties cho phép chúng ta tính giá trị dẫn xuất một cách mô tả. Tuy nhiên, có những trường hợp khi chúng ta cần thực hiện "hiệu ứng phụ" để phản ứng với sự thay đổi của trạng thái - ví dụ, thay đổi DOM, hoặc thay đổi một phần trạng thái khác dựa trên kết quả của một hoạt động không đồng bộ.

Với Options API, chúng ta có thể sử dụng watch option để kích hoạt một hàm mỗi khi một thuộc tính có phản ứng thay đổi:

js
export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)',
      loading: false
    }
  },
  watch: {
    // mỗi khi question thay đổi, hàm này sẽ chạy
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      } finally {
        this.loading = false
      }
    }
  }
}
template
<p>
  Hỏi một câu hỏi có thể trả lời bằng "có" hoặc "không":
  <input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>

Try it in the Playground

Tùy chọn watch cũng hỗ trợ một đường dẫn có dấu chấm như là key:

js
export default {
  watch: {
    // Lưu ý: chỉ là đường dẫn đơn giản. Không hỗ trợ biểu thức.
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

Với Composition API, chúng ta có thể sử dụng watch function để kích hoạt một callback mỗi khi một phần của trạng thái có phản ứng thay đổi:

vue
<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// watch hoạt động trực tiếp trên ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    Hỏi một câu hỏi có thể trả lời bằng "có" hoặc "không":
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

Try it in the Playground

Loại Nguồn của Watch

Đối số đầu tiên của watch có thể là các loại "nguồn" có phản ứng khác nhau: nó có thể là một ref (bao gồm cả computed refs), một đối tượng phản ứng, một hàm getter, hoặc một mảng của nhiều nguồn:

js
const x = ref(0)
const y = ref(0)

// một ref đơn
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// mảng của nhiều nguồn
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

Lưu ý rằng bạn không thể theo dõi một thuộc tính của một đối tượng có phản ứng như sau:

js
const obj = reactive({ count: 0 })

// điều này sẽ không hoạt động vì chúng ta đang truyền một số cho watch()
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})

Thay vào đó, sử dụng một getter:

js
// thay vào đó, sử dụng một getter:
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

Người Theo Dõi Sâu

watch là theo dõi nông bởi mặc định: hàm callback chỉ sẽ kích hoạt khi thuộc tính được theo dõi được gán một giá trị mới - nó sẽ không kích hoạt trên các thay đổi của thuộc tính lồng nhau. Nếu bạn muốn callback được kích hoạt trên tất cả các biến đổi lồng nhau, bạn cần sử dụng một người theo dõi sâu:

js
export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // Lưu ý: `newValue` sẽ bằng `oldValue` ở đây
        // trên các biến đổi lồng nhau miễn là đối tượng chính nó
        // chưa được thay thế.
      },
      deep: true
    }
  }
}

Khi bạn gọi watch() trực tiếp trên một đối tượng có phản ứng, nó sẽ tự động tạo một người theo dõi sâu - callback sẽ được kích hoạt trên tất cả các biến đổi lồng nhau:

js
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // kích hoạt trên các biến đổi thuộc tính lồng nhau
  // Lưu ý: `newValue` sẽ bằng `oldValue` ở đây
  // bởi vì cả hai đều trỏ đến cùng một đối tượng!
})

obj.count++

Điều này nên được phân biệt với một getter trả về một đối tượng có phản ứng - trong trường hợp sau, callback chỉ sẽ kích hoạt nếu getter trả về một đối tượng khác nhau:

js
watch(
  () => state.someObject,
  () => {
    // kích hoạt chỉ khi state.someObject được thay thế
  }
)

Tuy nhiên, bạn có thể buộc trường hợp thứ hai trở thành một người theo dõi sâu bằng cách sử dụng tùy chọn deep một cách tường minh:

js
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // Lưu ý: `newValue` sẽ bằng `oldValue` ở đây
    // *trừ khi* state.someObject đã được thay thế
  },
  { deep: true }
)

Sử Dụng Cẩn Thận

Người theo dõi sâu yêu cầu duyệt qua tất cả các thuộc tính lồng nhau trong đối tượng được theo dõi và có thể tốn kém khi sử dụng trên các cấu trúc dữ liệu lớn. Sử dụng nó chỉ khi cần thiết và lưu ý đến ảnh hưởng về hiệu suất.

Người Theo Dõi Nhanh

watch là lười biếng theo mặc định: hàm callback sẽ không được gọi cho đến khi nguồn theo dõi đã thay đổi. Nhưng trong một số trường hợp, chúng ta có thể muốn logic callback tương tự được chạy ngay lập tức - ví dụ, chúng ta có thể muốn tải một số dữ liệu ban đầu, và sau đó tải lại dữ liệu mỗi khi trạng thái liên quan thay đổi.

Chúng ta có thể buộc callback của người theo dõi được thực thi ngay lập tức bằng cách khai báo nó bằng một đối tượng với một hàm handler và tùy chọn immediate: true:

js
export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // điều này sẽ được chạy ngay lập tức khi component được tạo.
      },
      // buộc thực thi callback ngay lập tức
      immediate: true
    }
  }
  // ...
}

Việc thực thi ban đầu của hàm xử lý sẽ xảy ra ngay trước created hook. Vue đã xử lý các tùy chọn data, computed, và methods, nên những thuộc tính đó sẽ có sẵn trong lần gọi đầu tiên.

Chúng ta có thể buộc callback của người theo dõi được thực thi ngay lập tức bằng cách truyền tùy chọn immediate: true:

js
watch(
  source,
  (newValue, oldValue) => {
    // thực thi ngay lập tức, sau đó lại khi `source` thay đổi
  },
  { immediate: true }
)

watchEffect()

Thường xuyên, hàm callback của người theo dõi sẽ sử dụng chính giá trị trạng thái phản ứng giống như nguồn theo dõi. Ví dụ, xem xét đoạn mã sau, sử dụng một người theo dõi để tải nguồn từ xa mỗi khi giá trị todoId thay đổi:

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 }
)

Đặc biệt, hãy chú ý cách người theo dõi sử dụng todoId hai lần, một lần làm nguồn và sau đó làm lời gọi lại.

Điều này có thể được đơn giản hóa với watchEffect(). watchEffect() cho phép chúng ta theo dõi các phụ thuộc của hàm callback một cách tự động. Người theo dõi ở trên có thể được viết lại như sau:

js
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

Ở đây, callback sẽ chạy ngay lập tức, không cần phải chỉ định immediate: true. Trong quá trình thực thi của nó, nó sẽ tự động theo dõi todoId.value như một phụ thuộc (tương tự như computed properties). Khi todoId.value thay đổi, callback sẽ được chạy lại. Với watchEffect(), chúng ta không còn cần phải truyền todoId một cách rõ ràng như một giá trị nguồn.

Bạn có thể kiểm tra ví dụ này về watchEffect() và việc tải dữ liệu phản ứng trong thực tế.

Đối với những ví dụ như vậy, với chỉ một phụ thuộc, lợi ích của watchEffect() tương đối nhỏ. Nhưng đối với người theo dõi có nhiều phụ thuộc, sử dụng watchEffect() loại bỏ gánh nặng phải duy trì danh sách các phụ thuộc một cách thủ công. Ngoài ra, nếu bạn cần theo dõi một số thuộc tính trong một cấu trúc dữ liệu lồng nhau, watchEffect() có thể hiệu quả hơn so với một người theo dõi sâu, vì nó chỉ theo dõi các thuộc tính được sử dụng trong callback, thay vì đệ quy theo dõi tất cả chúng.

TIP

watchEffect chỉ theo dõi các phụ thuộc trong quá trình đồng bộ thực thi của nó. Khi sử dụng nó với một callback async, chỉ có các thuộc tính được truy cập trước lần await đầu tiên sẽ được theo dõi.

watch so với watchEffect

watchwatchEffect đều cho phép chúng ta thực hiện các hiệu ứng phụ theo cách phản ứng. Sự khác biệt chính giữa chúng là cách chúng theo dõi các phụ thuộc của mình:

  • watch chỉ theo dõi nguồn được xem theo dõi một cách rõ ràng. Nó sẽ không theo dõi bất kỳ thứ gì được truy cập bên trong callback. Ngoài ra, callback chỉ kích hoạt khi nguồn thực sự đã thay đổi. watch phân tách theo dõi phụ thuộc từ hiệu ứng phụ, mang lại sự kiểm soát chính xác hơn về thời điểm mà callback nên chạy.

  • watchEffect, ngược lại, kết hợp theo dõi phụ thuộc và hiệu ứng phụ thành một giai đoạn. Nó tự động theo dõi mọi thuộc tính phản ứng được truy cập trong quá trình thực thi đồng bộ của nó. Điều này làm cho nó thuận tiện hơn và thường dẫn đến mã nguồn ngắn hơn, nhưng khiến cho các phụ thuộc phản ứng của nó trở nên ít rõ ràng hơn.

Thời gian Xử lý Gọi lại Đẩy

Khi bạn thay đổi trạng thái phản ứng, điều này có thể kích hoạt cả cập nhật thành phần Vue và các hàm callback theo dõi do người dùng tạo.

Mặc định, các hàm callback theo dõi được tạo bởi người dùng được gọi trước cập nhật thành phần Vue. Điều này có nghĩa là nếu bạn cố gắng truy cập DOM bên trong một hàm callback theo dõi, DOM sẽ ở trong trạng thái trước khi Vue áp dụng bất kỳ cập nhật nào.

Nếu bạn muốn truy cập DOM trong một hàm callback theo dõi sau khi Vue đã cập nhật nó, bạn cần chỉ định tùy chọn flush: 'post':

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

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

watchEffect() sau khi đẩy cũng có một bí danh tiện ích, watchPostEffect():

js
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* được thực thi sau khi Vue cập nhật */
})

this.$watch()

Bạn cũng có thể tạo các hàm theo dõi theo cách mệnh lệnh bằng cách sử dụng phương thức của một thể hiện $watch():

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

Điều này hữu ích khi bạn cần thiết lập một hàm theo dõi theo điều kiện, hoặc chỉ muốn theo dõi một số điều gì đó dựa trên tương tác của người dùng. Nó cũng cho phép bạn dừng theo dõi sớm nếu cần.

Dừng một Hàm theo dõi

Các hàm theo dõi được khai báo bằng cách sử dụng tùy chọn watch hoặc phương thức thể hiện $watch() sẽ tự động dừng khi thành phần chủ bị hủy, nên trong hầu hết các trường hợp, bạn không cần lo lắng về việc dừng hàm theo dõi.

Trong trường hợp hiếm hoi bạn cần dừng một hàm theo dõi trước khi thành phần chủ bị hủy, API $watch() sẽ trả về một hàm để thực hiện điều đó:

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

// ...khi hàm theo dõi không còn cần thiết nữa:
unwatch()

Các hàm theo dõi được khai báo đồng bộ bên trong setup() hoặc <script setup> được liên kết với thể hiện của thành phần chủ và sẽ tự động dừng khi thành phần chủ bị hủy. Trong hầu hết các trường hợp, bạn không cần lo lắng về việc dừng hàm theo dõi.

Điều quan trọng ở đây là hàm theo dõi phải được tạo ra đồng bộ: nếu hàm theo dõi được tạo trong một hàm gọi lại không đồng bộ, nó sẽ không được liên kết với thành phần chủ và phải được dừng thủ công để tránh rò rỉ bộ nhớ. Dưới đây là một ví dụ:

vue
<script setup>
import { watchEffect } from 'vue'

// Hàm này sẽ tự động dừng
watchEffect(() => {})

// ...hàm này sẽ không!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

Để dừng thủ công một hàm theo dõi, sử dụng hàm cánh quay được trả về. Điều này hoạt động cho cả watchwatchEffect:

js
const unwatch = watchEffect(() => {})

// ...sau này, khi không còn cần
unwatch()

Lưu ý rằng có rất ít trường hợp nơi bạn cần tạo hàm theo dõi một cách không đồng bộ, và tạo đồng bộ nên được ưu tiên mỗi khi có thể. Nếu bạn cần đợi một số dữ liệu không đồng bộ, bạn có thể làm cho logic của bạn theo dõi điều kiện:

js
// Dữ liệu để được tải không đồng bộ
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // làm một cái gì đó khi dữ liệu được tải
  }
})
Watchers has loaded