Bài viết được sự cho phép của tác giả Lưu Bình An
Rất nhiều thư viện Javascript như Angular, React, Vue sử dụng Reactivity, hiểu được reactivity là gì và cách nó chạy sẽ giúp nâng cao kỹ năng lập trình
Một ví dụ về Reactivity của Vue
<div id='app'>
<div>Price: ${{ price }}</div>
<div>Total: ${{ price * quantity }}</div>
<div>Taxes: ${{ totalPriceWithTax }}</div>
</div>
var vm = new Vue({
el: '#app',
data: {
price: 5.00,
quantity: 2
},
computed: {
totalPriceWithTax() {
return this.price * this.quantity * 1.03
}
}
})
Ở đây khi chúng ta thay đổi giá trị của price
, thằng Vue nó sẽ làm 3 thứ
- Cập nhập lại giá trị
price
- Tính lại giá trị
total
- Gọi lại hàm
totalPriceWithTax
và cập nhập lại giá trị
Thấy hết sức bình thường, nhưng đó KHÔNG PHẢI LÀ CÁCH CHẠY BÌNH THƯỜNG CỦA JAVASCRIPT
Ví dụ với javascript bình thường
let price = 5
let quantity = 2
let total = price * quantity // kết quả sẽ là 10
price = 20 // gán lại giá trị của price
console.log(`total is ${total}`)
Bạn hãy đoán xem kết quả log ra là mấy? Sẽ là 10 chứ không phải 40 đâu.
Tham khảo tuyển dụng javascript lương cao trên TopDev
Vấn đề và giải pháp
Vấn đề là chúng ta cần phải lưu cái cách tính price * quantity
này lại ở đâu đó, để chúng ta re-run cách tính này khi gọi lại total
, nó sẽ không nên là biến số mà là thành hàm, thì khi đó nếu giá trị price
hoặc quantity
thay đổi chúng ta sẽ có kết quả total
thay đổi theo.
Chúng ta cần một nơi để lưu phần code tính toán kiểu như vậy lại ở đâu đó, để khi price
hoặc quantity
thay đổi, chúng ta sẽ chạy lại tất cả những gì đã lưu
let price = 5;
let quantity = 2;
let total = 0;
let target = () => { total = price * quantity }
record(); // lưu lại đâu đó để re-run sau này
target(); //
Hàm record chúng ta sẽ implement nó như sau
let storage = []; // đưa toàn bộ các hàm muốn re-run vào mảng này
function record() {
storage.push(target);
}
// hàm để chạy lại tất cả những thứ đã lưu trong store
function replay() {
storage.forEach(run => run());
}
Giải pháp tổng quát hơn
Nếu đã nắm được ý tưởng chính để giải quyết bài toán ban đầu, giờ chúng ta sẽ hiện thực hóa nó bẳng observer pattern, tạo một class
để quản lý những chuyện đó
class Dep {
constructor() {
// thay vì là starage, thiên hạ đã thống nhất lấy cái tên subscribers
this.subscribers = [];
}
depend() {
if (target && !this.subscribers.includes(target)) {
// chỉ thêm vào nếu chưa có hoặc không trùng
thiss.subscribers.push(target);
}
}
notify() {
// run tất cả target, tên gọi khác là observer
this.subscribers.forEach(sub => sub());
}
}
Code lại ví dụ trên sử dụng class
mới tạo này
const dep = new Dep();
let price = 5;
let quantity = 2;
let total = 0;
let target = () => { total = price * quantity }
dep.depend();
target();
console.log(total); // 10
price = 20;
console.log(total); // 10
dep.notify();
console.log(total); // 40
Chúng ta vẫn còn có thể nâng cấp đoạn code trên, thay vì
let target = () => { total = price * quantity }
dep.depend();
target();
… chúng ta đóng gói nó vào một watcher, sau đó chỉ cần gọi
watcher(() => {
total = price * quantity
})
Implement cái function watcher này như bên dưới
function watcher(myFunc) {
target = myFunc; // active target, target ở đây là global variable
dep.depend(); // đưa target vào dependency
target(); // gọi hàm target
target = null; // reset
}
Tách Dep cho mỗi biến
Chúng ta sẽ muốn mỗi một biến có một Dep
riêng, trước tiên ta đưa price
và quantity
thành property của data
let data = {price: 5, quantity: 2}
Chúng ta sẽ có các Dep khác nhau cho price
và quantity
watcher phụ thuộc cả 2 biến
watcher(() => {
total = data.price * data.quantity;
})
watcher chỉ phụ thuộc biến price
watcher(() => {
salePrice = data.price * 0.9;
})
Chúng ta muốn khi giá trị price
bị thay đổi, hàm dep.notify
của price store
sẽ được gọi
>> total
10
>> price = 20 // lúc này thằng notify của price sẽ được gọi liên luôn
>> total
40
Đọc thêm tài liệu về Object.defineProperty nếu chưa biết. Áp dụng nó trong ví dụ này
let data = {price: 5, quantity: 2}
let internalValue = data.price; // giá trị khởi tạo
Object.defineProperty(data, 'price', { // chỉ cho thằng Price Property
get() {
console.log('Em bị access');
return internalValue;
},
set(newVal) {
console.log('Em bị thay đổi');
internalvalue = newVal;
}
})
data.price // call get()
data.price = 20 // call set()
total = data.price * data.quantity;
data.price = 20;
Với cách này, chúng ta có thể chạy kèm một hàm nào đó khi giá trị price
được get hoặc set. Với idea là như thế chúng ta tổng quát quá lên cho nhiều biến
let data = {price: 5, quantity: 2}
Object.keys(data).forEach(key => {
let intervalvalue = data[key];
Object.defineProperty(data, key, { // chỉ cho thằng Price Property
get() {
console.log('Em bị access');
return internalValue;
},
set(newVal) {
console.log('Em bị thay đổi');
internalvalue = newVal;
}
})
})
total = data.price * data.quantity;
data.price = 20;
Tổng hợp các ý tưởng chính
total = data.price * data.quantity
Khi một đoạn code như vậy được chạy, nó sẽ get
giá trị của price
, chúng ta muốn thẳng price
khi bị thay đổi hoặc gọi, nó sẽ re-run một function
- Ở Get: nhớ dùm cái function này, bọn tao sẽ nhờ mày chạy lại
- Ở Set: chạy cái function mày đã giữ hộ ấy, thay đổi giá trị luôn nhé
Và đây là toàn bộ code
let data = {price: 5, quantity: 2};
let target = null;
// Dep không thay đổi gì so với ở trên
class Dep {
constructor() {
// thay vì là starage, thiên hạ đã thống nhất lấy cái tên subscribers
this.subscribers = [];
}
depend() {
if (target && !this.subscribers.includes(target)) {
// chỉ thêm vào nếu chưa có hoặc không trùng
thiss.subscribers.push(target);
}
}
notify() {
// run tất cả target, tên gọi khác là observer
this.subscribers.forEach(sub => sub());
}
}
// chạy qua từng data của property
Object.keys(data).forEach(key => {
let intervalvalue = data[key];
// mỗi em một Dep
const dep = new Dep();
Object.defineProperty(data, key, { // chỉ cho thằng Price Property
get() {
dep.depend(); // lưu hộ tao cái
return internalValue;
},
set(newVal) {
internalvalue = newVal;
dep.notify();// re-run đi em
}
})
})
// watcher sẽ không còn gọi dep.depend nữa
function watcher(myFunc) {
target = myFunc;
target();
target = null;
}
watcher(() => {
data.total = data.price * data.quantity;
})
Bài viết gốc được đăng tải tại Vuilaptrinh
Tuyển dụng lập trình viên lương cao trên TopDev