JavaScript: Mutation Observer
Bài đăng này đã không được cập nhật trong 5 năm
Cú pháp
MutationObserver
sử dụng cực kỳ đơn giản. Chúng ta chỉ cần tạo ra
một đối tượng observer với các callback tương ứng:
let observer = new MutationObserver(callback);
Sau khi đã có observer rồi, chúng ta chỉ cần gắn nó với DOM cần quan sát là đủ:
observer.observe(node, config);
Trong phương thức trên, config
là một đối tượng với các giá trị dạng
boolean, ứng với các key sau, mang ý nghĩa "thành phần nào của DOM
thay đổi sẽ được gọi callback":
childList
- Thay đổi trong các thành phần con trực tiếp củanode
.subtree
- Mọi thành phần trongnode
attributes
- các thuộc tính củanode
,attributeOldValue
- Ghi lại giá trị cũ của các thuộc tính (config này kết hợp với configattributes
ở trên),characterData
- Có quan sátnode.data
(nội dung của các thẻ) hay khôngcharacterDataOldValue
- Ghi lại giá trị cũ củanode.data
(dùng kết hợp vớicharacterData
),attributeFilter
- Một dánh sách các thuộc tính observer sẽ quan sát.
Sau khi có bất kỳ thay đổi nào trên DOM theo config trên, callback
sẽ được gọi thực thi với hai tham số: tham số thứ nhất là một danh
sách các MutationRecord
và tham số thứ hai chính là đối tượng observer.
MutationRecord là những đối tượng có thuộc tính như sau:
type
- là một trong số các giá trị (chỉ thuộc tính nào thay đổi)"attributes"
: thuộc tính dã thay đổi"characterData"
: dữ liệu đã thay đổi (chỉ áp dụng với node text)"childList"
: các thành phần con thay đổi (thêm/bớt)
target
- Sự thay đổi xảy ra ở đâuaddedNodes/removedNodes
- các node được thêm/bớtpreviousSibling/nextSibling
- Các anh em phía trước/sau của node được thêm/bớtattributeName/attributeNamespace
- tên hoặc namespace (áp dụng với XML) của thuộc tính bị thay đổi.oldValue
- Giá trị trước khi thay đổi, chỉ áp dụng với thuộc tính hoặc text.
Ví dụ, chúng ta có một div
với thuộc tính contentEditable
như sau,
chúng ta có thể tập trung vào đó:
<div contentEditable id="elem">Click and <b>edit</b>, please</div>
<script>
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(the changes)
});
observer.observe(elem, {
// observe everything except attributes
childList: true,
subtree: true,
characterDataOldValue: true
});
</script>
Nếu chúng ta thay đổi text bên trong <b>edit</b>
chúng ta sẽ gặp một
thuộc tính MutationRecord
:
mutationRecords = [{
type: "characterData",
oldValue: "edit",
target: <text node>,
// other properties empty
}];
Nếu chúng ta xoá bỏ thành phần <b>edit</b>
, chúng ta sẽ gặp nhiều
MutationRecord
hơn:
mutationRecords = [{
type: "childList",
target: <div#elem>,
removedNodes: [<b>],
nextSibling: <text node>,
previousSibling: <text node>
// other properties empty
}, {
type: "characterData"
target: <text node>
// ...details depend on how the browser handles the change
// it may coalesce two adjacent text nodes "edit " and ", please" into one node
// or it can just delete the extra space after "edit".
// may be one mutation or a few
}];
Cách sử dụng
Chúng ta cần sử dụng MutationObserver
khi nào? Đó là một câu hỏi
khó. Vì thực tế, từng node trên DOM cũng có thể trigger event và gọi
callback khi có thay đổi rồi.
Dưới đây là một số trường hợp mà MutationObserver
sẽ cần thiết:
Chúng ta cần nó để theo dõi một số thuộc tính, ví dụ contentEditable
và cài đặt tính năng undo/redo. Lúc này, MutationObserver
sẽ là
thích hợp hơn cả.
Thử tưởng tượng chúng ta xây dựng một website về lập trình. Lẽ tự nhiên là các đoạn code sẽ thường xuyên xuất hiện trên trang web đó, có thể có dạng như sau:
...
<pre class="language-javascript"><code>
// here's the code
let hello = "world";
</code></pre>
...
Ở đây, để highlight code cho đẹp, chúng ta có thể sử dụng một số thư
viện JavaScript, ví dụ Prism.js. Thế nhưng để
gọi phương thức Prism.highlightElem(pre)
của thư viện này, thời điểm
là cực kỳ quan trọng.
Chúng ta có thể gọi theo event DOMContentLoaded
, hoặc ở dưới cùng
của trang, khi mà toàn bộ HTML đã được tải hết.
// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);
Sau khi gọi như trên thì đoạn code sẽ được highlight (dưới đây chỉ mang tính chất minh hoạ):
// here's the code
let hello = "world";
Mọi việc rất đơn giản, chúng ta gọi thư viện để highlight code trong
các thẻ pre
. Thế nhưng mọi thứ sẽ phức tạp hơn khi mà chúng ta có
những tính năng ví dụ như lấy code từ một server bên ngoài.
let article = /* fetch new content from server */
articleElem.innerHTML = article;
Giá trị của article
sẽ bao gồm các đoạn code cần highlight. Chúng
ta cần gọi thư viện Prism.highlightElem
, nếu không code trông sẽ rất
xấu. Vấn đề bây giờ phức tạp hơn, bởi việc gọi thư viện
Prism.highlightElem
sẽ phụ thuộc vào việc tải dữ liệu từ server
ngoài.
Chúng ta có thể thực viện việc đó sau khi tải xong như sau:
let article = /* fetch new content from server */
articleElem.innerHTML = article;
*!*
let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);
*/!*
Nhưng nếu chúng ta có rất nhiều những thành phần như vậy trên một trang, chúng ta phải gọi phương thức highlight code đó ở bất cứ đâu. Đó quả là một sự bất tiện không hề nhẹ.
Rất may mắn là MutationObserver
cho chúng ta một phương án khác tốt
hơn. Chúng ta có thể sử dụng nó để tự động theo dõi quá trình thay
đổi của DOM (khi nào các đoạn code được thêm vào) và thực hiện
highlight chúng.
Như vậy, việc highlight code chỉ cần thực hiện ở một nơi, không hề cần phải lo lắng liệu chúng ta có thể quên mất không gọi nó ở nơi nào hay không.
Ví dụ
Dưới đây là một ví dụ thực tế, bằng việc sử dụng observer như sau, chúng ta sẽ quan sát các thành phần của DOM và thực hiện highlight code mỗi khi chúng xuất hiện:
let observer = new MutationObserver(mutations => {
for(let mutation of mutations) {
// examine new nodes
for(let node of mutation.addedNodes) {
// we track only elements, skip other nodes (e.g. text nodes)
if (!(node instanceof HTMLElement)) continue;
// check the inserted element for being a code snippet
if (node.matches('pre[class*="language-"]')) {
Prism.highlightElement(node);
}
// maybe there's a code snippet somewhere in its subtree?
for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
Prism.highlightElement(elem);
}
}
}
});
let demoElem = document.getElementById('highlight-demo');
observer.observe(demoElem, {childList: true, subtree: true});
Đoạn code dưới đây sẽ thực hiện điền innerHTML
cho các thành phần.
Chạy thử đoạn code trên vào đoạn dưới đây, bạn sẽ thấy điều kỳ diệu:
let demoElem = document.getElementById('highlight-demo');
// dynamically insert content with code snippets
demoElem.innerHTML = `A code snippet is below:
<pre class="language-javascript"><code> let hello = "world!"; </code></pre>
<div>Another one:</div>
<div>
<pre class="language-css"><code>.class { margin: 5px; } </code></pre>
</div>
`;
Giờ đây, chúng ta có một MutationObserver
có thể quan sát DOM và
thực hiện highlight code theo đúng yêu cầu, chúng ta có thể load bao
nhiêu code rồi hiển thị tuỳ ý, mà không cần phải lo về việc highlight
chúng nữa.
Các phương thức khác
Chúng ta có thể dừng việc quan sát của một observer lại như sau:
observer.disconnect()
Ngoài ra, chúng ta có thể sử dụng:
mutationRecords = observer.takeRecords()
để lấy ra danh sách các MutationRecord chưa được xử lý (những sự thay đổi đã xảy ra, nhưng callback chưa xử lý chúng).
// we're going to disconnect the observer
// it might have not yet handled some mutations
let mutationRecords = observer.takeRecords();
// process mutationRecords
// now all handled, disconnect
observer.disconnect();
Kết luận
MutationObserver
có thể quan sát dự thay đổi của DOM và chúng ta có
thể sử dụng nó để theo dõi các thay đổi đó, đồng thời có hành động
tương ứng phù hợp.
All rights reserved