Cafedev hân hạnh chia sẻ với độc giả về Vuejs và cách sử dụng Watchers trong ứng dụng web. Vuejs đã trở thành một trong những framework phổ biến nhất cho phát triển giao diện người dùng hiện đại, và Watchers là một phần quan trọng của cách Vuejs quản lý và phản ứng với thay đổi trong dữ liệu. Bằng cách sử dụng Watchers, bạn có thể theo dõi và xử lý các thay đổi trong dữ liệu một cách linh hoạt và hiệu quả. Hãy cùng khám phá chi tiết về Vuejs và Watchers trong bài viết dưới đây!

1. Ví dụ Cơ bản

Các thuộc tính tính toán cho phép chúng ta tính toán giá trị dẫn xuất theo cách khai báo. Tuy nhiên, có những trường hợp chúng ta cần thực hiện “hiệu ứng phụ” để phản ứng với các trạng thái thay đổ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 bất đồ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 reactive thay đổi:

export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)',
      loading: false
    }
  },
  watch: {
    // whenever question changes, this function will run
    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
      }
    }
  }
}
<p>
  Ask a yes/no question:
  <input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>

Thử nghiệm trong Playground
Option watch cũng hỗ trợ một đường dẫn được phân cách bằng dấu chấm như là khóa:

export default {
  watch: {
    // Note: only simple paths. Expressions are not supported.
    '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 reactive thay đổi:

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

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

// watch works directly on a 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>
    Ask a yes/no question:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

Thử nghiệm trong Playground

1.1 Các Loại Quan Sát

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

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

// array of multiple sources
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

Hãy nhớ rằng bạn không thể quan sát một thuộc tính của một đối tượng reactive như này:

const obj = reactive({ count: 0 })

// this won't work because we are passing a number to watch()
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})

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

// instead, use a getter:
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

2. Quan Sát Sâu

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

export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // Note: `newValue` will be equal to `oldValue` here
        // on nested mutations as long as the object itself
        // hasn't been replaced.
      },
      deep: true
    }
  }
}

Khi bạn gọi watch() trực tiếp trên một đối tượng reactive, nó sẽ tạo ra một quan sát sâu ngầm – callback sẽ được kích hoạt trên tất cả các thay đổi lồng nhau:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // fires on nested property mutations
  // Note: `newValue` will be equal to `oldValue` here
  // because they both point to the same object!
})

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 reactive – trong trường hợp cuối cùng, callback chỉ được kích hoạt nếu getter trả về một đối tượng khác:

watch(
  () => state.someObject,
  () => {
    // fires only when state.someObject is replaced
  }
)

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

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // Note: `newValue` will be equal to `oldValue` here
    // *unless* state.someObject has been replaced
  },
  { deep: true }
)

Cảnh báo Sử dụng Cẩn thận Quan sát sâu yêu cầu đi qua tất cả các thuộc tính lồng nhau trong đối tượng được quan sát, và có thể tốn kém khi sử dụng trên cấu trúc dữ liệu lớn. Sử dụng chỉ khi cần thiết và hãy cẩn thận với các ảnh hưởng về hiệu suất.

3. Quan Sát Nhanh

watch mặc định là lười biếng: callback sẽ không được gọi cho đến khi nguồn được quan sát đã thay đổi. Nhưng trong một số trường hợp, chúng ta có thể muốn cùng một logic callback được chạy một cách hấp tấp – ví dụ, chúng ta có thể muốn lấy một số dữ liệu ban đầu, và sau đó lấy 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 một quan sát được thực thi ngay lập tức bằng cách khai báo nó sử dụng một đối tượng có một hàm handler và tùy chọn immediate: true:

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

Việc thực thi ban đầu của hàm xử lý sẽ diễn ra ngay trước hook created. Vue sẽ đã xử lý các tùy chọn data, computed, và methods, vì vậy 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 một quan sát được thực thi ngay lập tức bằng cách truyền tùy chọn immediate: true:

watch(
  source,
  (newValue, oldValue) => {
    // executed immediately, then again when `source` changes
  },
  { immediate: true }
)

4. Quan Sát Một Lần

Callback của Quan Sát sẽ được thực thi mỗi khi nguồn được quan sát thay đổi. Nếu bạn muốn callback chỉ kích hoạt một lần duy nhất khi nguồn thay đổi, hãy sử dụng tùy chọn once: true.

export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // when `source` changes, triggers only once
      },
      once: true
    }
  }
}
watch(
  source,
  (newValue, oldValue) => {
    // when `source` changes, triggers only once
  },
  { once: true }
)

5. watchEffect()

Thông thường, callback của watcher sử dụng chính xác cùng một trạng thái reactive như nguồn. Ví dụ, xem xét đoạn mã sau, sử dụng một watcher để tải một tài nguyên từ xa mỗi khi todoId ref thay đổi:

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, chú ý cách watcher sử dụng todoId hai lần, một lần làm nguồn và sau đó lại trong callback.
Đ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 reactive của callback tự động. Watcher ở trên có thể được viết lại như sau:

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ư là một phụ thuộc (tương tự như các thuộc tính tính toán). Mỗi khi todoId.value thay đổi, callback sẽ được chạy lại. Với watchEffect(), chúng ta không cần phải truyền todoId một cách rõ ràng như là giá trị nguồn nữa.
Bạn có thể xem ví dụ về ví dụ này về watchEffect() và lấy dữ liệu reactive.

Đối với các ví dụ như thế này, chỉ có một phụ thuộc, lợi ích của watchEffect() tương đối nhỏ. Nhưng đối với các watcher có nhiều phụ thuộc, việc sử dụng watchEffect() loại bỏ gánh nặng của việc 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 quan sát nhiều 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 một watcher sâu, vì nó chỉ theo dõi các thuộc tính được sử dụng trong callback, thay vì theo dõi tất cả chúng một cách đệ quy.

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

5.1 watch so với watchEffect

watchwatchEffect đều cho phép chúng ta thực hiện các hiệu ứng phụ một cách phản ứng. Sự khác biệt chính là cách chúng theo dõi các phụ thuộc reactive của chúng:
watch chỉ theo dõi nguồn được quan sát một cách rõ ràng. Nó sẽ không theo dõi bất kỳ điều gì được truy cập bên trong callback. Ngoài ra, callback chỉ được kích hoạt khi nguồn thực sự thay đổi. watch phân tách việc theo dõi phụ thuộc khỏi hiệu ứng phụ, cho chúng ta kiểm soát chính xác hơn về khi nào callback nên được kích hoạt.
watchEffect, từ phía khác, kết hợp việc theo dõi phụ thuộc và hiệu ứng phụ vào một giai đoạn. Nó tự động theo dõi mọi thuộc tính reactive được truy cập trong quá trình thực thi đồng bộ của nó. Điều này tiện lợi hơn và thường dẫn đến mã nguồn ngắn gọn hơn, nhưng làm cho các phụ thuộc reactive của nó ít rõ ràng hơn.

6. Thời Điểm Kích hoạt Callback

Khi bạn thay đổi trạng thái reactive, nó có thể kích hoạt cập nhật cảu component Vue và các callback watcher do bạn tạo.
Tương tự như cập nhật component, các callback watcher được tạo bởi người dùng cũng được gom nhóm để tránh gọi hai lần. Ví dụ, chúng ta có lẽ không muốn một watcher kích hoạt một nghìn lần nếu chúng ta đồng bộ thêm một nghìn mục vào một mảng đang được quan sát.

Theo mặc định, callback của một watcher được gọi sau cập nhật component cha (nếu có), và trước cập nhật DOM của component chủ sở hữu. Điều này có nghĩa là nếu bạn cố gắng truy cập DOM của component chủ sở hữu bên trong một callback của watcher, DOM sẽ ở trong trạng thái trước cập nhật.

6.1 Post Watchers

Nếu bạn muốn truy cập DOM của component chủ sở hữu trong một callback của watcher sau khi Vue đã cập nhật nó, bạn cần chỉ định tùy chọn flush: 'post':


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

watch(source, callback, {
  flush: 'post'
})

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

watchEffect() sau-flush cũng có một tên gọi thuận tiện, watchPostEffect():

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* executed after Vue updates */
})

6.2 Sync Watchers

Cũng có thể tạo ra một watcher kích hoạt đồng bộ, trước bất kỳ cập nhật nào được quản lý bởi Vue:


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

watch(source, callback, {
  flush: 'sync'
})

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

watchEffect() đồng bộ cũng có một tên gọi thuận tiện, watchSyncEffect():

import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* executed synchronously upon reactive data change */
})

Cảnh báo: Sử dụng cẩn thận Watchers đồng bộ không có batching và kích hoạt mỗi khi phát hiện một biến đổi reactive. Việc sử dụng chúng để theo dõi các giá trị boolean đơn giản là hợp lý, nhưng tránh sử dụng chúng trên các nguồn dữ liệu có thể bị biến đổi đồng bộ nhiều lần, ví dụ, các mảng.

7. this.$watch()

Cũng có thể tạo watchers một cách mệnh lệnh bằng cách sử dụng phương thức thể hiện <code$watch()>:

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 watcher theo điều kiện, hoặc chỉ muốn theo dõi một cái gì đó phản ứng với tương tác của người dùng. Nó cũng cho phép bạn dừng lại watcher sớm.

8. Dừng một Watcher

Watchers đượ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 lại khi component chủ sở hữu bị gỡ bỏ, vì vậy trong hầu hết các trường hợp bạn không cần phải lo lắng về việc dừng lại watcher một cách thủ công.
Trong trường hợp hiếm hoi mà bạn cần dừng lại một watcher trước khi component chủ sở hữu bị gỡ bỏ, API $watch() sẽ trả về một hàm cho việc đó:

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

// ...when the watcher is no longer needed:
unwatch()

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

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

// this one will be automatically stopped
watchEffect(() => {})

// ...this one will not!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

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

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

// ...later, when no longer needed
unwatch()

Lưu ý rằng chỉ có rất ít trường hợp bạn cần tạo watchers bất đồng bộ, và việc tạo ra đồng bộ nên được ưu tiên mỗi khi có thể. Nếu bạn cần chờ đợi một số dữ liệu bất đồng bộ, bạn có thể làm cho logic watch của mình điều kiện thay vì.

// data to be loaded asynchronously
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // do something when data is loaded
  }
})

Trong bài viết này, chúng tôi hy vọng đã cung cấp cho bạn cái nhìn tổng quan và sâu sắc về cách sử dụng Watchers trong Vuejs. Sự linh hoạt và mạnh mẽ của Watchers giúp bạn theo dõi và phản ứng với các thay đổi trong dữ liệu một cách hiệu quả, từ đó tạo ra các ứng dụng web đáng tin cậy và mạnh mẽ. Hãy áp dụng những kiến thức này vào dự án của bạn và trải nghiệm sức mạnh của Vuejs cùng Cafedev!

Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Vuejs từ cơ bản tới nâng cao

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!