React sử dụng Virtual DOM như thế nào?
Tiếp tục về chủ đề React và Virtual DOM, ở bài viết trước chúng ta đã tìm hiểu tại sao Virtual DOM được dùng bởi React, TLDR:
- Nhóm DOM update khi có thay đổi
- Tạo abstraction giúp ngừoi dùng không phải tự tay update DOM
- Tối ưu update DOM
Ở bài viết này chúng ta sẽ đi sâu hơn vào khía cạnh giúp React tối ưu việc update DOM.
DOM – Document Object Modal
DOM là dữ liệu native của browser, được parse và build từ source code, sau đó sử dụng để render lên màn hình – DOM là đại diện cho chính trang web của chúng ta.
DOM thể hiện trang web dưới dạng các nodes và objects, DOM cung cấp interfaces giúp ngôn ngữ như JS có thể điều khiển được trang web.
Reconciliation
Với mỗi app React, ta có thể coi thể hiện của nó là một cây elements, khi state hoặc props thay đổi, React tạo ra một cây mới, so sánh 2 cây này để xác định các phần UI cần cập nhật, sau đó thực hiện các cập nhật này lên DOM thật.
Quá trình này gọi là Reconciliation.
Vậy quá trình so sánh trên diễn ra như thế nào?
Vấn đề tìm kiếm các bước tối thiểu đế biến 1 cây thành 1 cây khác tuy có lời giải tổng quát nhưng lại có độ phức tạp O(n^3). Do vậy React sử dụng thuật toán heuristic có độ phức tạp O(n) dựa trên 2 giả định sau:
- 2 elements khác
types
sẽ tạo ra 2 cây khác nhau - developer có thể chỉ định elements nào cần thay đổi thông qua khoá
key
Thuật toán so sánh
Khi so sánh 2 cây elements, React bắt đầu bằng việc so sánh 2 elements gốc. Kết quả phụ thuộc vào type
của 2 element đó.
2 elements khác type
Khi bắt gặp 2 elements gốc khác type
, React sẽ vứt bỏ toàn bộ cây cũ, khởi tạo lại cây mới từ đầu và thay thế vào vị trí đó.
Ví dụ như thay đổi từ thẻ <a>
sang thẻ <img>
, hay từ component <Article>
sang <Comment>
, hoặc từ <Button>
sang <div>
– tất cả đều sẽ được đập đi xây lại từ đầu.
Khi cây element cũ bị đập đi, DOM nodes tương ứng bị xoá đi. Khi cây element mới được tạo ra, DOM nodes mới được thêm vào. Toàn bộ state của cây element cũ cũng sẽ mất đi.
Các elements con của elements gốc cũng giống như vậy, bị xoá hoàn toàn và khởi tạo lại từ đầu, toàn bộ state cũ sẽ biến mất.
Ví dụ, ta có thay đổi sau
<div> <Counter /> </div> <span> <Counter /> </span>
Component Counter
sẽ bị đập đi xây lại, sinh ra bản thể mới.
2 DOM elements cùng type
Khi gặp 2 DOM element cùng type
React sẽ giữ nguyên DOM node, chỉ thay đổi những thuộc tính của node đó
<div className="before" title="samestuff" /> <div className="after" title="samestuff" />
Sau khi so sánh React biết chỉ có className là thay đổi và sẽ cập nhật lên DOM thật.
Sau khi cập nhật DOM node của element gốc, React tiếp tục đệ quy quá trình so sánh đến các element con.
2 component element cùng type
Khi so sánh component element cùng type
, React giữ nguyên bản thể của element, như vậy state được giữ lại trong suốt quá trình. render()
được gọi để sinh ra cây element con của bản thân element này, sau đó quá trình so sánh tiếp tục đệ quy.
Đệ quy element con
Khi so sánh 2 danh sách element con của một DOM node, React đơn giản chỉ chạy qua cả 2 danh cùng một lúc, thấy thay đổi là ghi lại.
Ví dụ khi thêm 1 element vào cuối danh sách
<ul> <li>first</li> <li>second</li> </ul> <ul> <li>first</li> <li>second</li> <li>third</li> </ul>
React thấy rằng 2 element đầu tiên giống nhau nên giữ nguyên, đến element thứ 2 thì chỉ cây mới là có, vì thế React gọi DOM và thêm 1 node mới.
Tuy nhiên điều gì sảy ra nếu ta thêm 1 element vào đầu danh sách
<ul> <li>apple</li> <li>orange</li> </ul> <ul> <li>grapes</li> <li>apple</li> <li>orange</li> </ul>
Khi chạy qua danh sách và so sánh lần lượt, sẽ chẳng có phần nào là giống nhau, React sẽ gọi DOM xoá lần lượt từng node ở cây cũ, sau đó thêm lần lượt node ở cây mới, như vậy rõ ràng không tối ưu vì không tận dụng được phần đã có.
Để giải quyết vấn đề này chính là giả định thứ 2 của thuật toán: cung cấp 1 khoá key
cho element con trong một danh sách.
<ul>
<li key="101">apple</li>
<li key="102">orange</li>
</ul>
<ul>
<li key="100">grapes</li>
<li key="101">apple</li>
<li key="102">orange</li>
</ul>
khi được cung cấp key
, React biết apple
và orange
đã có sẵn chỉ bị di chuyển vị trí mà thôi, grapes
là element mới, vì thế React chỉ cần gọi DOM và thêm node grapes
vào đầu danh sách.
Việc chọn key
cho danh sách con rất quan trọng, tối ưu nhất là luôn sinh ra unique ID hoặc permanent ID khi dữ liệu được sinh ra, và không nên chọn key
chính là array index của dữ liệu
Nếu một ngày đẹp trời bạn phải làm chức năng thêm/xoá/kéo thả input field và thấy value chạy lung tung giữa các field, hãy thử kiểm tra lại key
của <input>
nhé.
Kết
Như vậy chúng ta đã cưỡi ngựa xem hoa thêm một phần ẩn dưới cách React hoạt động, hi vọng sau bài viết này bạn có thể mường tượng cách React giúp chúng ta rảnh tay useState
mà không cần querySelector
hay getDocumentById
, và hãy luôn nhớ sử dụng key
một cách tối ưu nhé.