NỘI DUNG BÀI VIẾT
Proxy trong ES6
The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).
— MDN
Tên Proxy làm chúng ta nhớ lại mấy cái proxy bên mạng, nó đóng vai trò trung gian giữa người request và người nhận request. Cũng tương tự như vậy, Proxy trong ES6 này cũng đóng vai trò trung gian giữa target object và nơi thực thi. Dùng để custom các behavious của các operator cơ bản.
Proxy gồm có ba phần:
- Target object: Object mà bạn áp dụng proxy lên nó, nó chứa các dữ liệu thật
- Traps: Các phương thức tương ứng dùng để hồi đáp lại các “yêu cầu” thao tác dữ liệu/gọi hàm trên Target
- Handler: Là object chứa các traps
Cú pháp
let p = new Proxy(target, handler)
Code language: JavaScript (javascript)
Hãy cho một ví dụ để có hứng thú tiếp tục:
let oldProfile = {
name: 'Anh Nguyen',
score: 0,
life: 1
}
let handler = {
get: (target, name) => {
console.log(name)
return name === 'score' ? 1000 : target[name0]
}
}
let newProfile = new Proxy(oldProfile, handler)
console.log(newProfile.score)
Code language: JavaScript (javascript)
Target object chỉ là một object bình thường, chúng ta không bàn về nó ở đây, chỉ có handler và trap là khá mới lạ, trong đó Handler cũng chỉ là một object để chứa traps mà thôi.
Traps
Traps là các phương thức của handler và được gọi ra để xử lý các yêu cầu truy cập lên target.
Chúng ta có khá nhiều trap, các trap này đều là tùy chọn, nếu bạn không định nghĩa trap nào, thì các yêu cầu sẽ được chuyển qua cho target xử lý.
Chúng ta có các trap như sau:
- handler.getPrototypeOf()
- handler.setPrototypeOf()
- handler.isExtensible()
- handler.preventExtensions()
- handler.getOwnPropertyDescriptor()
- handler.defineProperty()
- handler.has()
- handler.get()
- handler.set()
- handler.deleteProperty()
- handler.ownKeys()
- handler.apply()
- handler.construct()
Các trap này sẽ được gọi ra tương ứng với các thao tác mà bạn gọi lên proxy. Như ở ví dụ đầu bài, khi lấy giá trị của property thì phương thức get
sẽ được gọi kèm theo hai tham số là target và tên của property.
Lý thuyết thì có chừng đó, bây giờ chúng ta hãy thử làm một vài ví dụ.
Object Validation
Giả sử bạn muốn validate thuộc tính age
của một object:
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
// Indicate success
return true;
},
};
let person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // Throws an exception
person.age = 30; // Throws an exception
Code language: JavaScript (javascript)
Reactive DOM
Nếu dùng Vuejs bạn sẽ thấy tính năng reactive của nó trên DOM rất thú vị. Ví dụ như bạn thay đổi giá trị của biến và nó tự động cập nhật lại DOM. Bạn cũng hoàn toàn có thể làm điều đó với Proxy.
<div id="js-score">0</div>
<button id="js-increase">increase</button>
<script>
let sourceProfile = {
name: 'Anh Nguyen',
score: 0
}
let handler = {
set (target, property, value) {
if (property === 'score') {
document.getElementById('js-score').innerHTML = value
}
target[property] = value
return true
}
}
let profile = new Proxy(sourceProfile, handler)
document.getElementById('js-increase').addEventListener('click', (event) => {
event.preventDefault()
profile.score++
})
</script>
Code language: HTML, XML (xml)
Thú vị nhỉ. Nhưng để làm được như Vuejs thì còn một chặn đường quá xa. Tương lai Vuejs cũng sẽ sử dụng proxy thay vì sử dụng Object.defineProperty(), và bạn không cần dùng hàm $set khi muốn thêm một property chưa được định nghĩa trước.
Cache
Trong Vuejs cũng có cơ chế để cache giá trị của các phương thức. Giả sử chúng ta cũng gặp trường hợp tương tự, chúng ta có một phương thức cần rất nhiều tính toán, chúng ta cần cache lại kết quả nếu như tham số của hai lần gọi là giống hệt nhau:
let handler = {
prevNumber: 0,
cachedValue: 0,
apply (target, thisArg, argumentsList) {
if (this.prevNumber === argumentsList[0]) {
return this.cachedValue
}
this.prevNumber = argumentsList[0]
return this.cachedValue = Reflect.apply(target, thisArg, argumentsList)
}
}
function expensiveCompute(number){
console.count('Real compute!')
return number * 10
}
let proxyExpensiveCompute = new Proxy(expensiveCompute, handler)
console.log(proxyExpensiveCompute(11))
console.log(proxyExpensiveCompute(12))
console.log(proxyExpensiveCompute(12))
Code language: JavaScript (javascript)
Khi thực thi, thì Real Compute!
chỉ chạy hai lần. Hai dòng console cuối cùng có thể bị browser hoặc RunkIt merge vào nhau, nhưng thực tế là hàm tính toán thực sự đã không được gọi, mà giá trị trong cache đã được gọi ra.
Đây chỉ là ví dụ thôi, chứ không thực sự có thể ứng dụng được vào project của bạn, nếu muốn dùng nó thì có lẽ bạn sẽ cần tải tiến nhiều hơn.
API Object
Làm API object cũng rất thú vị. Giả sử như bạn có một kết nối realtime, bạn muốn các object sync với nhau mà không cần phải gọi đi gọi làm phương thức emit. Lúc này bạn cần làm một proxy, và code phần update trong trap set. Phần này chỉ là ý tưởng, các bạn tự hiện thực.
Lời kết
Như bạn thấy trong ví dụ của mình có sử dụng Reflect, nó và proxy hỗ tương. Nếu proxy là để custom behavious thì Reflect dùng để thực hiện default behavious của target. Chúng ta sẽ nói về nó ở những bài viết sau.
Còn bây giờ hay tìm cái gì đó vui vui làm với Proxy đi nào. Đọc xong làm liền thì dễ hiểu hơn.
Cảm ơn các bạn đã theo dõi, xin nhờ các bạn đóng góp thêm ý kiến hoặc những ví dụ thú vị hơn về proxy.
Các bạn có thể tham khảo các bài viết hay về JavaScript tại đây.
Hãy tham gia nhóm Học lập trình để thảo luận thêm về các vấn đề cùng quan tâm.