Appearance
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 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>
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
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 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
watch
và watchEffect
đề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
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 */
})
Dừng một Hàm theo dõi
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ả watch
và watchEffect
:
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
}
})