Immutability in React.js
Nếu đã từng tìm hiểu về " Functional Programing ", hẳn bạn đã từng nghe thấy thuật ngữ " Immutability - Tính bất biến ". Đây là 1 trong những tính chất quan trọng nhất của Functional Programing . 1 đối tượng bất biến là 1 đối tượng không thể thay đổi trạng thái sau khi đã được khởi tạo ...
Nếu đã từng tìm hiểu về "Functional Programing", hẳn bạn đã từng nghe thấy thuật ngữ "Immutability - Tính bất biến". Đây là 1 trong những tính chất quan trọng nhất của Functional Programing.
1 đối tượng bất biến là 1 đối tượng không thể thay đổi trạng thái sau khi đã được khởi tạo
Trong React.js (và cả Redux) thì đây là một trong những tư tưởng thiết kế mang tính nền tảng. Mặc dù có thể đã nghe nhiều về lợi ích của Immutability nhưng không nhiều bạn hiểu rõ câu hỏi "WHY", tại sao chúng ta cần làm như vậy? Bài này chúng ta sẽ trả lời câu hỏi Immutability có mặt ở đâu React.js và giúp gì nhé.
Để hiểu được ý tưởng về Immutability (hay Data Immutability), tôi có 1 ví dụ nhỏ hết sức đơn giản:
var x = 12, y = 12; var object = { x: 1, y: 2 }; var object2 = { x: 1, y: 2 };
Như đã thấy thì object và object2 là 2 đối tượng có keys lẫn value bằng nhau, về mặt cấu trúc cũng tương đương nhau. Nhưng khi bạn check bằng toán tử == hoặc ===, kết quả dễ thấy các ngôn ngữ lập trình đều đánh gía 2 đối tượng này không như nhau.
object == object2 // false object === object2 // false
Đó là bởi vì, các ngôn ngữ lập trình đánh giá các phép so sánh dựa trên 2 loại:
- Reference Equality
- Value Equality
Tất cả mọi cấu trúc dữ liệu phức tạp trong JS đều tuân theo quy luật Reference Equality bao gồm : Array và Object (Array thì cũng là Object). Các Primitive Value thì khác nó tuân theo Value Equality
Reference equality
Đối tượng là những cấu trúc dữ liệu phức tạp. Chúng có thể có nhiều keys, mỗi key lại chỏ tới các value tùy chọn. Tất nhiên, value cũng có thể là 1 đối tượng khác. Nói cách khác các đối tượng có thể được lồng (nested) vào nhau. Khi so sánh các sự vật, hiện tượng với nhau ta thường đi đến kêt quả bằng việc trả lời câu hỏi:
A bằng B hay không?
Điều này phát sinh ra 2 chiến thuật giải quyết bài toán:
- A và B chính xác là 1 sự vật hiện tượng duy nhất.
- A có thể hiện tương đương với vế B.
Trong thực tế, có rất nhiều tình huống ta có thể áp dụng lý thuyết này. Hãy tưởng tượng bạn và hàng xóm mỗi người có 1 chiếc xe hơi. 2 chiếc xe cùng 1 model, giống hệt nhau về màu sắc và chủng loại. Nhưng chắc chắn bạn không thể ngồi lên chiếc xe của anh hàng xóm mà lái coi như xe của mình được. Có thể thấy tuy thể hiện tương đương nhau, nhưng đây là 2 chiếc xe khác nhau, và để phân biệt chúng người ta sử dụng "Giấy Đăng Ký Xe". Javascript (và nhiều ngôn ngữ lập trình khác) cũng triển khai 1 cơ chế gần giống như vậy để quản lý đối tượng. Mỗi đối tượng sẽ có unique ID được gọi là Reference. Điều đó có nghĩa là khi ta so sánh như ở ví dụ đầu bài, ta đang trả lời câu hỏi object và object2 có chính xác là 1 đối tượng duy nhất không? Phép gán cũng hoàn toàn tuân theo nguyên tắc này.
object = object2; object == object2; // true object === object2; // true
Về mặt kỹ thuật, Reference Equality dễ thực hiện, và hiệu năng cũng tốt. Bởi vì các đối tượng được taọ ra ở các ô nhớ khác nhau nên unique ID của chúng cũng khác nhau, JS đơn giản thực hiện phép so sánh này để trả lời câu hỏi.
Value Equality
Đối với các Primitive Value trong JS (Number, String, null, undefined,..) chúng không được phép nested hay còn gọi là shallow data structure. Cấu trúc dữ liệu này lại rất hiệu quả khi sử dụng với Value Equality .
var x = 12, y = 12; x == y; // true x === y; // true
Dễ thấy sự khác nhau ở đây, Value Equality trả lời cho câu hỏi x có thể hiện tương đương với y. Mặc dù x và y được tạo ra ở các ô nhớ khác nhau nhưng giá trị (12) của chúng là như nhau. Vê mặt kỹ thuật, triển khai chiến thuật so sánh này khó khăn hơn với các dạng cấu trúc dữ liệu phức tạp (complex data structure). Các object có kết cấu phức tạp dẫn đến để so sánh 2 thể hiện có tương đương không ta thường đi đến các thuật toán đệ quy
// Input: an object1 and object2 // Output: true if an object1 is equal in terms of values to object2 valueEqual(object1, object2): object1keys = <list of keys of object1> object2keys = <list of keys of object2> return false if length(object1keys) != length(object2keys) for each key in object1keys: return false if key not in object2keys return false if typeof(object1[key]) != typeof(object2[key]) if object1[key] is an object: keyEqual = valueEqual(object1[key], object2[key]) return false if keyEqual != false if object1[key] is a primitive: return false if object1[key] != object2[key] return true
2 đối tượng đơn giản cũng có thể phát sinh hàng ngàn phép so sánh, ta gọi đây là deep equality checks. Trong nhiều trường hợp, thuật toán này sẽ là vô hạn khi đối tượng tham chiếu tới chính mình, ví dụ đơn giản là khi bạn tạo ra đối tượng bằng Constructor Function, [[prototype]] của nó tham chiếu tới chính Constructor Function và cứ như vậy..
Ý nghĩa với React.js
React.js dựa vào 1 khái niệm gọi là state để quản lý / detect khi cần thiết thay đổi DOM. Nhờ vậy công việc update DOM trở nên đơn giản hơn - Bất cứ khi nào state thay đổi, component sẽ render lại. Nói đơn giản thì bạn chỉ có trách nhiệm thay đổi state, việc transform DOM là của React.js.
Việc thay đổi state luôn dẫn đến kết quả là render lại component vì React không thể quản lý việc bạn thao tác như thế nào với state. Điều này dẫn tới hệ quả tiếp theo: Khi có 1 state phức tạp với các đối tượng lồng nhau, mọi thứ trở nên khó khăn hơn để kiểm tra liệu state đã bị thay đổi hay chưa? Như đã biết để so sánh giá trị 2 đối tượng (array) phức tạp ta thường nghĩ tới việc sử dụng deep equality check .Mà như đã biết chiến thuật này phải thực hiện đến hàng ngàn phép thử. Ngoài ra thì, việc kiểm tra list thay đổi ra sao khi render list trong component cũng là 1 vấn đề khác khi áp dụng deep equality check. Những sự phiền phức này gây ra đó là bởi trong JavaScript bạn có thể mutate object.(đương nhiên là trong nhiều NNLT khác cũng vậy)
Data structure mutations là dạng cấu trúc dữ liệu điển hình trong các ngôn ngữ imperative như JavaScript. Khi bạn thay đổi 1 thuộc tính, reference của nó vẫn giữ nguyên. Đây là 1 tính chất khá là natural: cho dù bạn sơn xe của mình thành màu xanh, đỏ, tím... thì giấy đăng ký xe vẫn là số ban đầu.
var yourCar = { color: 'red', .. the same as neighboursCar }; var neighboursCar = { color: 'red', ... the same as yourCar }; valueEqual(yourCar, neighboursCar); // true; yourCar === neighboursCar; // false yourCar.color = 'blue'; valueEqual(yourCar, neighboursCar); // false; yourCar === neighboursCar; // false
mutations không thay đổi kết quả của reference equality checks mà là kết quả của value equality checks.
Quay trở lại với bài toán detect sự thay đổi của state trong React.js, chúng ta thực sự không quan tâm đến việc chính xác cái gì đã thay đổi mà cần câu trả lời cho câu hỏi state có thay đổi hay không. Rõ ràng chỉ cần áp dụng so sánh bằng reference equality để thực hiện có re-render lại component không là cách đơn giản và tốn ít tài nguyên nhất tiếc thay mutation như 1 cách quen thuộc ta vẫn làm từ trước tới nay không hữu dụng trong tình huống này. May mắn là Immutability có mặt ở đây giải quyết vấn đề theo cách đơn giản nhất, bằng cách tạo ra đối tượng mới mang reference mới, React.js có thể áp dụng reference equality và vấn đề được giải quyết nhanh chóng và dễ dàng.
1 đối tượng bất biến là 1 đối tượng không thể thay đổi trạng thái sau khi đã được khởi tạo
Vậy khi cần lưu trữ/ thay đổi trạng thái ta làm thế nào? Thay vì thay đổi trực tiếp trên đối tượng đó như cách truyền thống, ta sẽ tạo ra 1 đối tượng mới, hết sức đơn giản. ES6 cũng đã giới thiệu các syntax mới mang lại hướng tiếp cận immutability trong code của bạn.
Object.assign({}, ...)
var yourCar = { color: 'red', .. the same as neighboursCar }; var neighboursCar = { color: 'red', ... the same as yourCar }; //yourCar.color = 'blue'; // Không làm thế này var yourCarRepainted = Object.assign({}, yourCar, { color: 'blue' }); // ngon hơn rồi ! yourCarRepainted === yourCar; // false
Object.assign() trả về đối tượng mới, và lẽ dĩ nhiên reference của nó khác với đối tượng cũ.
[].concat()
Tương tự với Object và spread operator (...), để thao tác với Array, ta dùng hàm concat()
var list = [1, 2, 3]; var changedList = [].concat(list); changedList[1] = 2; list === changedList; // false
React.js (và cả Redux) ứng dụng nhiều Functional Programing trong core của mình, và nó cũng đã chứng minh được Functional Programing là tốt hơn OOP. Trong bài này, mình giới thiệu sơ qua về ứng dụng cơ bản nhưng cũng rất kinh điển về việc check state của React.js được hưởng lợi thế nào từ Immutability, lần tới mình sẽ tiếp tục đi sâu hơn vào lợi và hại của Immutability, giới thiệu về các bộ library về immutability cũng như ứng dụng của nó trong Redux, (trong Redux thì sẽ có nhiều điều để nói hơn như là time-travel hay debug, multi-action... ^_^)
Refs: http://reactkungfu.com/2015/08/pros-and-cons-of-using-immutability-with-react-js/