Reactivity là gì? Học nhanh Proxy và Reflect qua ví dụ thực tế

Bây giờ là lúc để đi tìm hiểu sâu hơn vào bên trong các framework JS. Một trong những tính năng đặc biệt nhất của Vue hay Angular là hệ thống reactivity. Khi bạn thay đổi state thì view sẽ tự động được cập nhật. Điều này làm cho việc quản lý state trở nên dễ dàng và trực quan.

React không phải là thư viện reactive nhé. Bạn thay đổi state và gọi setState thì view mới cập nhật được, tức là bạn phải cập nhật bằng tay.

Reactivity là gì?

Reactivity là một mô hình lập trình cho phép chúng ta phản ứng lại những sự thay đổi biến. Excel sheet dưới đây là một ví dụ kinh điển về nó.

Nếu bạn nhập số 2 ở ô thứ nhất, số 3 ở ô thứ 2 và thực hiện SUM, sheet sẽ cho bạn giá trị tổng. Không có bất ngờ nào ở đây. Nhưng nếu bạn update giá trị thứ nhất, SUM cũng sẽ tự động update lại.

Javascript thì thường không hoạt động giống như vậy. Cùng viết thứ một đoạn bằng JS nhé.

js
let val1 = 2
let val2 = 3
let sum = val1 + val2
console.log(sum) // 5
val1 = 3
console.log(sum) // Still 5

Nếu chúng ta update giá trị đầu tiên, sum sẽ không bị thay đổi.

Vậy chúng ta làm điều này trong Javascript như thế nào?

Tạo một function handle và gọi lại bằng tay mỗi khi val1 hoặc val2 thay đổi ư, quá mệt mỏi!

js
let val1 = 2
let val2 = 3
let sum
const handle = () => {
sụm = val1 + val2
}
handle()
console.log(sum) // 5
val1 = 3
sum()
console.log(sum) // 6

Có một vài thứ chúng ta có thể làm là:

  1. Giám sát các giá trị. Ví dụ val1 + val2 thì giám sát cả 2 val1val2
  2. Phát hiện khi một giá trị thay đổi. Ví dụ chúng ta gán val1 = 3.
  3. Chạy lại code. Ví dụ run sum = val1 + val2 lại để cập nhật giá trị sum.

Và đây là cách giải quyết đơn giản.

js
let data = {
val1: 2,
val2: 3
}
let sum
const handle = () => {
sum = data.val1 + data.val2
}
handle()
Object.keys(data).forEach(key => {
let internalValue = data[key]
Object.defineProperty(data, key, {
get() {
return internalValue
},
set(newValue) {
internalValue = newValue
handle()
}
})
})
console.log(sum) // 5
data.val1 = 4
console.log(sum) // 7

Đầu tiên mình sẽ đưa những giá trị mình cần giám sát vào trong object data. Mình sẽ dùng Object.defineProperty để chuyển các thuộc tính dữ liệu của data thành thuộc tính bộ truy cập (getter / setter đó).

Các bạn có thể đọc thêm Property getters and setters để hiểu rõ hơn.

Và mỗi khi mình thực hiện thay đổi thuộc tính thì setter sẽ chạy và function handle() được thực thi.

Thế thôi, đơn giản mà đúng không

Nhưng ES6 cung cấp cho bạn một tính năng hay hơn chuyên để xử lý những vấn đề này, đó là Proxy.

Proxy là gì?

Proxy là một object thường được sử dụng để chỉnh sửa các hành vi của các toán tử cơ bản cho object.

Proxy thường được coi như là một trung gian giữa object và các hành vi gán, sửa, xóa… trên object.

Proxy được tạo với 2 tham số:

  • target: object gốc mà bạn muốn proxy
  • handler: một object chứa các trap. Trap là các phương thức để đáp ứng lại các thao tác dữ liệu trên target.

Quay trở lại bài toán bên trên, ta có thể giải quyết bằng proxy như sau

js
let data = {
val1: 2,
val2: 3
}
let sum
const handle = () => {
sum = data.val1 + data.val2
}
handle()
console.log(sum) // 5
const proxy = new Proxy(data, {
get(target, prop) {
return target[prop]
},
set(obj, prop, value) {
obj[prop] = value
handle()
// Phải return true để cho Engine biết là gán hoàn thành
// Nếu return false thì trong strict-mode sẽ bị lỗi TypeError
return true
}
})
proxy.val1 = 3
console.log(sum) // 6

Chúng ta cũng có thể xóa get() đi cũng được.

js
const proxy = new Proxy(data, {
set(obj, prop, value) {
obj[prop] = value
handle()
return true
}
})

get()set() bên trên được gọi là các trap.

Dưới đây là danh sách các trap theo trang mozilla

  • handler.apply(): A trap for a function call.

  • handler.construct(): A trap for the new operator.

  • handler.defineProperty(): A trap for Object.defineProperty.

  • handler.deleteProperty(): A trap for the delete operator.

  • handler.get(): A trap for getting property values.

  • handler.getOwnPropertyDescriptor(): A trap for Object.getOwnPropertyDescriptor.

  • handler.getPrototypeOf(): A trap for Object.getPrototypeOf.

  • handler.has(): A trap for the in operator.

  • handler.isExtensible(): A trap for Object.isExtensible.

  • handler.ownKeys(): A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols.

  • handler.preventExtensions(): A trap for Object.preventExtensions.

  • handler.set(): A trap for setting property values.

  • handler.setPrototypeOf(): A trap for Object.setPrototypeOf.

Tạo thử Reactive DOM như Vue 3

Nếu bạn đã code Vue thì bạn sẽ thấy hệ thống reactivity của Vue khá xịn, thay đổi data là nó sẽ giúp chúng ta cập nhật DOM luôn. Bây giờ mình sẽ mô phỏng lại tính năng đó thử xem sao nhé.

Khá là hay ho :mrgreen: .

Cho bạn nào chưa biết thì Vue 2 dùng Object.defineProperty, còn Vue 3 đã nâng cấp lên dùng Proxy rồi.

Nhắc đến Proxy thì không thể nào quên Reflect được, cùng mình tìm hiểu thử Reflect là gì nhé.

Reflect là gì?

Reflect là một object được cung cấp sẵn trong Javascript thường được dùng để phối hợp với proxy handler. Không như hầu hết các global object thì Reflect không phải là một constructor function, vì thế không thể tạo mới object với toán tử new.

Reflect cung cấp cho chúng ta các phương thức tương tự như bên Object.

js
const target = {
name: 'Dư Thanh Được'
}
// Tương tự Object.defineProperty
const result = Reflect.defineProperty(target, 'age', { value: 25 })
if (result) {
// Thành công
} else {
// Thất bại
}

Các phương thức của Reflect cũng tương ứng với các trap của Proxy, quay trở lại bài toán tính sum ban đầu, nếu áp dụng Reflect vào thì code sẽ như thế này.

js
const proxy = new Proxy(data, {
get(...props) {
Reflect.get(...props)
},
set(...props) {
const result = Reflect.set(...props)
handle()
return result
}
})

Tóm lại

Hy vọng bài viết ngắn này của mình giúp ích cho các bạn hiểu rõ hơn về Reactivity cũng như ProxyReflect.

Dạo này mình cũng cố gắng siêng hơn một chút, ra bài viết nhiều hơn một chút nên mọi người đón đọc nhé. Hẹn mọi người ở các bài viết tiếp theo 😀

Tham khảo