Giải thích Javascript Reactivity

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ứ

  1. Cập nhập lại giá trị price
  2. Tính lại giá trị total
  3. 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.

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 pricequantity thành property của data

let data = {price: 5, quantity: 2}

Chúng ta sẽ có các Dep khác nhau cho pricequantity

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;
})

Kết quả nè

Hình mình họa lấy từ Vue

Link bài gốc

Giải thích Javascript Reactivity