Với các bạn tìm hiểu về Web Development, chắc hẳn không xa lạ gì với khái niệm DOM?
Hay cụ thể hơn như là Original DOM
nhỉ?
Ngoài các trang web nhỏ và vừa ra thì có bao giờ bạn tự hỏi, các trang web lớn như Facebook, Twitter, GMail, etc thì có cả một “rừng cây Amazon” trong source code HTML
của mình thì họ quản lý DOM Tree
như thế nào không?
Và đương nhiên chỉ với DOM
thôi thì các nhà phát triển web khó lòng xử lý hàng núi việc như thế. Từ đó ra đời khái niệm Virtual DOM
và Shadow DOM
.
Đúng như tiêu đề thì ở bài viết này các bạn hãy cùng mình tìm hiểu sâu hơn về Original DOM
, Shadow DOM
và Virtual DOM
để có được cái nhìn đầy đủ hơn về chúng nhé.
Cùng bắt đầu thôi nào
Original DOM
Overview
Có thể bạn thừa biết, DOM là viết tắt của Document Object Modal.
DOM
bao gồm:
- Object-based representation of the HTML document
Các phần tử trong
DOM
có cấu trúc được định nghĩa thành các đối tượng, có các phương thức, thuộc tính để có thể truy xuất dễ dàng.Chúng được coi như các
node
và được biểu diễn dưới dạngDOM Tree
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <span class="token tag"><span class="token punctuation"><</span>html<span class="token punctuation">></span></span> -> document node <span class="token tag"><span class="token punctuation"><</span>head<span class="token punctuation">></span></span> -> element node - head <span class="token tag"><span class="token punctuation"><</span>title<span class="token punctuation">></span></span> HTML DOM -> text node <span class="token tag"><span class="token punctuation"></</span>title<span class="token punctuation">></span></span> -> element node - title <span class="token tag"><span class="token punctuation"></</span>head<span class="token punctuation">></span></span> <span class="token tag"><span class="token punctuation"><</span>body<span class="token punctuation">></span></span> -> element node - body <span class="token tag"><span class="token punctuation"></</span>body<span class="token punctuation">></span></span> <span class="token tag"><span class="token punctuation"></</span>html<span class="token punctuation">></span></span> |
- DOM APIs
HTML DOM
cung cấp cácAPIs
để duyệt và chỉnh sửa cácnode
.Ví dụ cụ thể về APIs có thể kể tới như
getElementById()
dùng để truy xuất một phần tử theoid
cũng hayremoveChild()
dùng để loại bỏ một phần tử con dựa vào phần tử cha của nó.
Shadow DOM
Overview
Mozilla
đã tóm tắt 3 đặc điểm cơ bản về Shadow DOM:
Shadow DOM can be thought of as:
- Isolated DOM
- A “lite” version of the DOM
- NOT of a full standalone document
Sở dĩ nói như vậy là do các stylesheet
hay javascript
từ bên ngoài không-thể-tác-động vào trong Shadow DOM
được và ngược lại.
Ví dụ như ta có thẻ <Video>
như một custom element.
Cấu trúc của một shadow DOM
Với Shadow DOM
chúng ta sẽ có thêm Shadow Root
. Nội dung Shadow Root
sẽ không được trình duyệt render
mà thay vào đó là Shadow Host
. Shadow Host
được xem như một element bình thường nhưng nội dung bên trong nó (cả stylesheet
) sẽ được đóng gói và độc lập với thế giới bên ngoài:
Tạo một shadow DOM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token tag"><span class="token punctuation"><</span>style<span class="token punctuation">></span></span><span class="token style language-css"> <span class="token selector">p</span> <span class="token punctuation">{</span> <span class="token property">color</span><span class="token punctuation">:</span> green <span class="token punctuation">}</span> </span><span class="token tag"><span class="token punctuation"></</span>style<span class="token punctuation">></span></span> <span class="token tag"><span class="token punctuation"><</span>p <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>sd-root<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>I am Shadow Root<span class="token tag"><span class="token punctuation"></</span>p<span class="token punctuation">></span></span> <span class="token tag"><span class="token punctuation"><</span>script<span class="token punctuation">></span></span><span class="token script language-javascript"> <span class="token keyword">const</span> host <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#sd-root'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> root <span class="token operator">=</span> host<span class="token punctuation">.</span><span class="token function">createShadowRoot</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> root<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token string">` <style> p { color: gray }</style> <p>Hello <strong>Shadow DOM</strong></p> `</span></span><span class="token punctuation">;</span> </span><span class="token tag"><span class="token punctuation"></</span>script<span class="token punctuation">></span></span> |
Bạn đoán xem, kết quả sẽ là gì nào . . .
Ở đây khi trình duyệt render xong chúng ta sẽ nhận được:
thay vì là
Khi Inspect
phần tử lên ta sẽ thấy như thế này đây:
Như vậy, chúng ta sẽ có vài nhận xét như sau:
Style
bên trongShadow DOM
dù conflict selector với bên ngoài nhưng không sao vì đặc tính củaShadow DOM
(scoped style sheet
)- Khi chúng ta truy xuất
.innerHTML()
của element#sd-root
thì ta CHỈ lấy được đoạn text khởi tạo: “I am Shadow Root” và cũng không-thể-tác-động được gì vào bên trongShadow Root
, cụ thể chính làShadow Host
.
Okay, chúng ta đã rõ hơn về
DOM
:
- Phần sáng của nó (hay thường được gọi là Light DOM) là cái chúng ta có thể tương tác được.
- Phần tối của nó, cái bóng (Shadow DOM) chỉ phản ánh lên nó sẽ được render thế nào khi
Light DOM
thay đổi.
Và theo lẽ đương nhiên, ta không-bao-giờ có thể chạm được vào cái bóng của một vật nào cả
DOM
ngon vậy thì sao lại có Virtual DOM
làm gì nhỉ ?
Mình cùng tìm hiểu tiếp nhé
Virtual DOM
Mặc dù concept
này đã xuất hiện từ khá lâu rồi nhưng nó chỉ mới trở nên phổ biến khi được sử dụng trong các thư viện nổi tiếng như ReactJS
, VueJS
…
Overview
Đúng như tên gọi của nó, DOM-ảo. Ảo, có nghĩa là không thật
Virtual-DOM là một Object chứa các thông tin cần thiết để tạo ra một DOM (Document Object Model).
Virtual DOM
có khả năng tính toán, cập nhật các node
mà không cần sử dụng DOM APIs
. Sau khi cập nhật trên DOM ảo
, các thay đổi sẽ được thực hiện với Original DOM
.
Như một vài đặc điểm mình đã nêu ở trên, Virtual DOM
được xem như một cách hiệu quả để giải quyết vấn đề cập nhật trên DOM
, không cần render
lại toàn bộ cả DOM Tree
.
Dưới đây là đặc tả một DOM Tree
:
thành một Virtual DOM
:
Trong thực tế, thay vì phải sử dụng toàn-bộ-object
, chúng ta thường làm việc với các object-nhỏ-hơn
tương ứng với các thành phần của Virtual DOM
.
Cơ chế hoạt động
Ban đầu, một đối tượng có nhiệm vụ như một snapshot
từ virtual DOM
sẽ được tạo ra:
Bản copy
này được sử dụng để tạo diff
để so sánh với bản virtual DOM
ban đầu, về cơ bản thì nó sẽ có dạng như thế này:
Từ đó, giúp Engine
biết rằng phải update
element nào trong original DOM
, chúng chỉ thực hiện update
khi có sự thay đổi.
Để thực hiện việc này chỉ cần sử dụng một vòng loop
với diff
cho dù là thêm một node
mới hay update node
cũ:
Sau đó, tiến hành cập nhật list
, viết lại toàn bộ template
, chạy lại hàm render()
, hiển thị thay đổi ra ngoài view
.
Mặc dù nói là toàn bộ
template
, song, chỉ những phần thực sự thay đổi mới được cập nhật. Các thay đổi được thực hiện cho đối tượng này sau đó được đối chiếu và sửa đổi đối vớioriginal DOM
.
Why use Virtual DOM?
Speed
Trước đây mình nghĩ rằng lý do nói sử dụng Virtual DOM
nhanh hơn Original DOM
là do việc set attribute cho object nó nhanh hơn việc update DOM, nhưng không phải các bạn ạ.
Thực tế là việc cập nhật DOM
chẳng tốn nhiều thời gian là bao so với việc cập nhật attribute cho object
.
Điều thật sự ảnh hưởng tới speed performance
ở đây là khi DOM
thay đổi, Browser
buộc phải ngồi tính lại CSS
, build layout, paint template…
Vì cấu trúc của DOM
là tree structure
, khi muốn thay đổi các element
và các thẻ con của nó, ta phải thông qua các Reflow/Layout
. Từ đó, các thay đổi được sẽ được re-painted
. Càng nhiều các item
phải reflow/repaint
, web của bạn sẽ càng load
chậm
Hmmm…
Để hiểu sâu hơn vấn đề này, chúng ta hãy tìm hiểu thêm một khái niệm nữa đó là về cơ chế binding
.
Data-binding
Khái niệm data-binding
có thể hiểu đơn giản là:
Data-binding
: Data in Model changes <=> View changes.
Data-binding trong DOM thật
Bây giờ ta lướt qua một chút các framework
có cơ chế data-binding
như BackboneJS
, AngularJS
, khi dữ liệu của Model Object
thay đổi, nó sẽ:
- Xác nhận các
event
+ cácView Object
liên quan - Tác động vào các phần tử
DOM
trênView
và phản ánh sự thay đổi dữ liệu đó.
Data-binding trong DOM ảo
Các
framework
sử dụngVirtual-DOM
,Virtual-DOM
vừa đóng vai trò làModel
, vừa đóng vai trò làView
Như vậy, khi Virtual-DOM
thay đổi, mọi sự thay đổi trên Model
đã kéo theo sự thay đổi trên View
và ngược lại.
Dù không tác-động-trực-tiếp vào các phần tử DOM
ở View
nhưng vẫn thực hiện được cơ chế data-binding
. Điều này làm cho tốc độ ứng dụng tăng lên đáng kể.
BONUS: Virtual DOM trong ReactJS
Dù phần này có hơi mở rộng so với tiêu đề bài viết một chút , nhưng hôm rồi mình tình cờ đọc được một bài viết về cách ReactJS
thao tác với DOM ảo
khá thú vị nên bonus vào để chúng mình hiểu sâu hơn về Virtual DOM
nhé ^^
Với một thư viện sử dụng DOM ảo
như ReactJS
, sau khi DOM thật được load lần đầu tiên, DOM ảo
cũng được React
tạo ra tương ứng với DOM thật
bên trên. Trong React, nó cũng được gọi là một Component với tree structure gồm các Component con bên trong.
Lưu ý:
Khi có một state
mới được set
, React
đánh dấu nó như là một dirty Component
(nghĩa là mỗi khi chúng ta gọi tới setState()
thì Component đó sẽ được đánh dấu là dirty)
Các event
khi ta thao tác với DOM
được React event listener
lắng nghe. Do đó, khi có event
nào, event
đó sẽ được gửi tới React event listener
và sau đó sẽ chạy một anonymous function()
Trong anonymous function()
, chúng ta gọi tới this.setState()
với một new state value
. Sau khi this.setState()
được chạy, Component đó được đánh dấu là dirty.
Okay, điều gì xảy ra tiếp theo:
Chạy qua Component lifecycle
Quan trọng nhất trong quá trình update ở đây chính là render()
, sau khi xây dựng lại Component, DOM ảo
được tạo lại và update DOM ảo
để tìm ra sự khác biệt*(như phần Cơ chế hoạt động mình trình bày phía trên)*, từ đó tìm ra cụ thể element thay đổi, và sau đó cập nhật chỉ những element đó ở DOM thật
.
Snapshots & Diffing
React
lấy một snapshot
của Virtual DOM
(bản ghi trạng thái ngay lúc đó) ngay trước khi áp dụng bất kỳ bản cập nhật nào. Sau đó, nó sử dụng snapshot
này để so sánh với một Virtual DOM được cập nhật trước khi thực hiện các thay đổi.
Khi cập nhật được cấp cho Virtual DOM
(Virtual DOM
sử dụng key
, ref
mà ở DOM
không có và Virtual DOM
được tạo mới sau mỗi lần render lại), quá trình tiếp theo React
sử dụng thuật toán Diffing
để so sánh và đối chiếu để biết được sự cập nhật được diễn ra ở đâu sau đó cập nhật nó mà bỏ qua những elements không liên quan.
Do đó, work-flow
của React
thao tác với DOM ảo
khi có change detection
:
- Xây dựng lại component
- Update DOM ảo
- Tìm sự thay đổi
- Update DOM thật
Nhờ có DOM ảo
, React
có thể tìm ra các node
bị thay đổi và update ở DOM thật
chỉ ở những cái node
đó, thật thuận tiện và nhanh gọn phải không nào ^^
Như vậy là chúng ta đã điểm qua Original DOM
, Shadow DOM
hay Virtual DOM
rồi. Hy vọng rằng bài viết này có thể giúp các bạn hiểu hơn về DOM
và các vấn đề liên quan.