Bài viết được sự cho phép của tác giả Kien Dang Chung
- Dữ liệu và tự bản thân nó thể hiện trong phần data, trong hệ thống reactivity.
- Tính toán, thao tác với dữ liệu đã có các computed property.
- Với mỗi dữ liệu, thực hiện các phương thức riêng đã có methods.
- Sự thay đổi của dữ liệu dẫn đến các thao tác đặc biệt thì xử lý thế nào?
Vấn đề cuối cùng có thể xử lý bằng computed property nhưng đây là các thuộc tính nên nó không có tham số đầu vào, chính vì vậy có một khái niệm nữa là watcher là những “đơn vị giám sát” dữ liệu trong thành phần data của Vue instance và thực hiện những công việc cần thiết để bổ sung công cụ cho chúng ta khi viết ứng dụng.
1. Watcher là gì trong framework Vue.js?
Đúng như tên gọi của nó, watcher giám sát các thay đổi trong một đối tượng, sử dụng watcher có thể có cùng kết quả với các giá trị được tính toán trước trong computed property nhưng với watcher nó phức tạp hơn. Chúng ta thường sử dụng watcher với những tình huống đòi hỏi phải xử lý phức tạp như:
- Các hoạt động bất đồng bộ đáp ứng lại việc thay đổi dữ liệu
- Các thiết lập giá trị ngay lập tức
- Hạn chế số lần thực hiện phương thức khi dữ liệu thay đổi.
Watcher có thể được khai báo trong thành phần watch của Vue instance, cú pháp như sau:
new
Vue
(
{
el:
'#app'
,
data:
{
}
,
watch:
{
}
}
)
;
Trong ví dụ tiếp theo, chúng ta xây dựng một ứng dụng giả lập công cụ tìm kiếm trực tuyến mà mỗi khi gõ vào một chuỗi truy vấn tìm kiếm ngay lập tức có câu trả lời. Hoạt động của ứng dụng này như sau, ứng dụng liên tục kiểm tra xem có sự thay đổi trong ô nhập truy vấn tìm kiếm hay không, nếu có nó sẽ gửi từ khóa đến một API để lấy câu trả lời. Chúng ta sẽ khai báo một phương thức giám sát (watcher) ô tìm kiếm.
new
Vue
(
{
el
:
‘#app’
,
data
:
{
searchQuery
:
”
,
results
:
[]
,
isTyping
:
false
}
,
watch
:
{
searchQuery
:
function
(
query
)
{
console
.
log
(
query
)
;
var
vm
=
this
;
setTimeout
(
function
(
)
{
console
.
log
(
‘setTimeout call’
)
;
vm
.
results
=
[‘Vue.js’
,
‘React’
,
‘Angular 2’
]
;
vm
.
isTyping
=
false
;
}
,
1000
)
;
}
}
}
)
;
Tiếp đến là phần quan trọng nhất trong đoạn code này, giám sát sự thay đổi của searchQuery và truyền chuỗi truy vấn người dùng nhập vào đến API xử lý.
watch:
{
searchQuery:
function
(
query)
{
var
vm =
this
;
setTimeout
(
function
(
)
{
vm.
results =
['Vue.js'
,
'React'
,
'Angular 2'
]
;
vm.
isSearching =
false
;
}
,
1000
)
;
}
}
Ở đây chúng ta đưa vào hàm setTimeout với thời gian chờ là 1 giây để giả lập thời gian xử lý truy vấn từ API, sau 1 giây kết quả trả về là một mảng tên các framework Javascript. Khi sử dụng hàm setTimeout có một chú ý là biến this sẽ trỏ đến đối tượng window chứ không phải trỏ đến Vue instance như chúng ta mong muốn. Như vậy chúng ta cần sao chép Vue instance sang một biến khác để tham chiếu bên trong hàm setTimeout().
Chương trình chạy ok nhưng có một vấn đề xảy ra, có quá nhiều lời gọi đến setTimeout không cần thiết khi mà người dùng gõ quá nhanh. Chúng ta cùng tìm hiểu vấn đề này:
Có quá nhiều lời gọi setTimeout trong một khoảng thời gian quá ngắn khi mà người dùng đang gõ chuỗi truy vấn, làm thế nào để khắc phục vấn đề này? Đây là một vấn đề rất hay gặp phải khi xử lý các đoạn mã bất đồng bộ. Chúng ta sử dụng đến hàm _.debound() là một hàm trong bộ thư viện lodash, nó cho phép ngừng một thời gian và chỉ gọi phương thức xử lý một lần sau khoảng thời gian đó. Code khi sử dụng lodash như sau:
new
Vue
(
{
el
:
‘#app’
,
data
:
{
searchQuery
:
”
,
results
:
[]
,
isSearching
:
false
}
,
watch
:
{
searchQuery
:
_
.
debounce
(
function
(
query
)
{
console
.
log
(
query
)
;
this
.
results
=
[‘Vue.js’
,
‘React’
,
‘Angular 2’
]
;
this
.
isSearching
=
false
;
}
,
1000
)
}
}
)
;
Bạn thấy đấy, watcher này vẫn giám sát sự thay đổi của searchQuery nhưng chỉ thực sự xử lý khi phương thức này tạm dừng một giây, tức là khi nào người dùng ngừng gõ 1 giây thì mới gửi chuỗi truy vấn đến API để xử lý. Với cách xử lý này, nó giúp cho hạn chế số lượng lời gọi đến API mà vẫn cho kết quả cuối cùng, các lời gọi khác do xử lý quá nhanh nên chưa kịp cho ra kết quả đã phải xử lý tiếp theo gây dư thừa xử lý trên phía máy chủ cung cấp API.
2. Tham số đầu vào của watcher
Trong ví dụ trên chúng ta thấy watcher cho searchQuery có tham số đầu vào là query chính là chuỗi truy vấn người dùng nhập vào, đây là giá trị mới nhất của searchQuery. Chúng ta cũng có thể lấy lại giá trị trước đó của searchQuery thông qua tham số thứ hai của watcher.
watch:
{
searchQuery:
function
(
newVal,
oldVal)
{
console.
log
(
newVal)
;
console.
log
(
oldVal)
;
}
}
Tham số đầu tiên của watcher là newVal sẽ chứa giá trị mới nhất của thuộc tính đang được “giám sát”, tham số thứ hai oldVal chứa giá trị trước khi có thay đổi của thuộc tính được giám sát. Watcher cũng có thể chỉ có một tham số đầu vào.
watch:
{
searchQuery:
function
(
newVal)
{
console.
log
(
newVal)
;
}
}
3. Giám sát thuộc tính được lồng bên trong đối tượng
Đối tượng trong thành phần data của Vue instance có thể rất phức tạp với các thuộc tính được lồng ở cấp sâu hơn, ví dụ:
data:
{
person:
{
name:
'Nguyễn Văn A'
,
drivingLicense:
{
id:
'GPLX0393928282'
,
issueDate:
'20180417'
,
issueBy:
'Bộ GTVT'
}
}
}
Khi đó nếu chúng ta muốn giám sát thuộc tính issueDate thì sử dụng cú pháp tham chiếu đến thuộc tính thông qua dấu chấm.
watch:
{
'person.drivingLicense.issueDate'
:
function
(
newVal,
oldVal)
{
alert
(
'Giấy phép được gia hạn từ '
+
oldVal +
' sang '
+
newVal)
;
}
}
Nếu bạn không muốn sử dụng cách tham chiếu với dấu chấm và dấu nháy đơn có thể gói nó vào trong một computed property:
computed:
{
licenseIsUpgrade
(
)
{
return
this
.
person.
drivingLicense.
issueDate;
}
}
,
watch:
{
licenseIsUpgrade:
function
(
newVal,
oldVal)
{
alert
(
'Giấy phép được gia hạn từ '
+
oldVal +
' sang '
+
newVal)
;
}
}
watch:
{
'person'
:
{
handler:
function
(
newVal,
oldVal)
{
console.
log
(
'Giám sát đối tượng'
,
' giá trị cũ: '
,
newVal,
' giá trị cũ:'
,
oldVal)
}
,
deep:
true
}
}
data:
{
person:
{
name:
'Nguyễn Văn A'
,
drivingLicense:
{
id:
'GPLX0393928282'
,
issueDate:
'20180417'
,
issueBy:
'Bộ GTVT'
}
}
}
computed:
{
clonePerson:
function
(
)
{
return
JSON.
parse
(
JSON.
stringify
(
this
.
person)
)
;
}
}
,
watch:
{
clonePerson:
function
(
newVal,
oldVal)
{
alert
(
JSON.
stringify
(
newVal)
)
;
alert
(
JSON.
stringify
(
oldVal)
)
;
}
}
4. Thêm hoặc hủy bỏ các watcher trong khi chạy ứng dụng
Khi khai báo các watcher chúng ta khai báo trong Vue Instance, vậy nếu chúng ta muốn thêm các watcher hoặc hủy bỏ chúng đi ở bên ngoài thực thể Vue thì sao? Bạn hoàn toàn yên tâm, Vue instance có biến $watch giúp bạn thực hiện công việc này.
var
vm
=
new
Vue
(
{
el
:
‘#app’
,
data
:
{
counter
:
1
}
}
)
;
vm
.
$watch
(
‘counter’
,
function
(
newValue
,
oldValue
)
{
alert
(
‘Bộ đếm tăng từ ‘
+
oldValue
+
‘ lên ‘
+
newValue
+
‘!’
)
;
}
)
;
Với watcher được thêm vào ở ngoài Vue instance cũng có tham số để thiết lập xem chúng ta giám sát cả đối tượng hay không?
vm.
$watch
(
'person'
,
function
(
newVal,
oldVal)
{
alert
(
'Nhân sự thay đổi từ '
+
oldVal.
fullName +
' sang '
+
newVal.
fullName +
'!'
)
;
}
,
{
deep:
true
}
)
;
Với các watcher phức tạp, có thể thay biểu thức tham chiếu đến một thuộc tính đối tượng thông qua toán tử dấu chấm bằng một hàm trả về biểu thức cần giám sát.
vm.
$watch
(
function
(
)
{
return
this
.
counter;
}
,
function
(
newValue,
oldValue)
{
alert
(
'Bộ đếm tăng từ '
+
oldValue +
' lên '
+
newValue +
'!'
)
;
}
)
;
Chúng ta đã nói đến việc thêm watcher ở ngoài Vue instance, vậy làm cách nào để Vue không giám sát tiếp các thuộc tính trong data? Rất đơn giản, phương thức $watch() trả về một phương thức mà nếu chúng ta gọi đến phương thức này, Vue sẽ không giám sát thuộc tính trong data nữa.
var
vm =
new
Vue
(
{
el:
'#app'
,
data:
{
counter:
1
}
}
)
;
var
unwatch =
vm.
$watch
(
function
(
)
{
return
this
.
counter;
}
,
function
(
newValue,
oldValue)
{
alert
(
'Bộ đếm tăng từ '
+
oldValue +
' lên '
+
newValue +
'!'
)
;
}
)
;
setTimeout
(
function
(
)
{
unwatch
(
)
;
}
,
10000
)
;
Sau 10 giây mọi thay đổi của counter sẽ không được giám sát nữa.
5. So sánh computed, watch và methods trong Vue instance
Chúng ta cùng xem xét một ví dụ sau:
var
vm =
new
Vue
(
{
el:
'#welcome'
,
data:
{
message:
'Hello'
,
name:
'World'
,
nameEdits:
0
}
,
computed:
{
welcomeMessage:
function
(
)
{
return
this
.
message +
' '
+
this
.
name
}
}
,
watch:
{
name:
function
(
)
{
if
(
this
.
message.
toLowerCase
(
)
===
'reset'
)
{
this
.
nameEdits =
0
}
else
{
this
.
nameEdits +
=
1
}
}
}
,
methods:
{
numRenders:
function
(
)
{
console.
log
(
'Page rendered'
)
}
}
}
)
Đầu tiên chúng ta xem phần computed, welcomeMessage chỉ được gọi khi message hoặc name thay đổi vì nó phụ thuộc vào thuộc tính này. Vì vậy, nếu nameEdits thay đổi, welcomeMessage cũng không được gọi. Thuộc tính Computed giúp ứng dụng hoạt động hiệu quả tối ưu mà vẫn có những phản ứng.
Tiếp theo chúng ta sang phần watch, nó đang giám sát thuộc tính name, có nghĩa là bất cứ khi nào name thay đổi, hàm khai báo sẽ được gọi để cập nhật nameEdits. Chú ý, mỗi watch chỉ giám sát trên một thuộc tính, ở đây là name, do đó khi nameEdits và message thay đổi, hàm này không được kích hoạt.
Cuối cùng, chúng ta xem phần methods, numRender được gọi mỗi khi render xong (cụ thể hơn là được gọi khi các đoạn mã gọi đến hoặc sự kiện người dùng khi giao diện đã được render), vì vậy bất cứ khi nào có gì đó được cập nhật trên giao diện người dùng, numberRenders được gọi. Ví dụ, nếu bạn có một ứng dụng hiển thị đồng hồ và nó cập nhật theo từng giây, tất cả các phương thức sẽ được gọi từng giây bất kể phương thức đó làm gì.
6. Bài tập
Continue