MutationObserver là một đối tượng tích hợp quan sát một phần tử DOM và kích hoạt một lệnh gọi lại trong trường hợp thay đổi.
Trước tiên, chúng ta sẽ xem xét cú pháp, và sau đó khám phá một trường hợp sử dụng trong thế giới thực, để xem điều đó có thể hữu ích ở đâu.
Nội dung chính
1. Cú pháp
MutationObserver rất dễ sử dụng.
Đầu tiên, chúng ta tạo một trình quan sát với một hàm gọi lại:
let observer = new MutationObserver(callback);
Và sau đó đính kèm nó vào một nút DOM:
observer.observe(node, config);
config là một đối tượng có các tùy chọn boolean “loại thay đổi nào cần phản ứng”:
- childList- những thay đổi về con cái trực tiếp của node,
- subtree- trong tất cả con cháu của node,
- attributes- thuộc tính của node,
- attributeFilter – một mảng tên thuộc tính, chỉ quan sát những tên thuộc tính đã chọn.
- characterData- có quan sát không node.data(nội dung văn bản),
Một số tùy chọn khác:
- attributeOldValue- nếu true, chuyển cả giá trị cũ và mới của thuộc tính để gọi lại (xem bên dưới), nếu không, chỉ cái mới (cần tùy chọn attributes),
- characterDataOldValue- nếu true, chuyển cả giá trị cũ và mới của node.data để gọi lại (xem bên dưới), nếu không, chỉ giá trị mới (cần tùy chọn characterData).
Sau đó, sau bất kỳ thay đổi nào, đối số callback sẽ được thực thi: các thay đổi được chuyển vào đối số đầu tiên dưới dạng danh sách các đối tượng MutationRecord và chính trình quan sát là đối số thứ hai.
Các đối tượng MutationRecord có các thuộc tính:
- type – loại đột biến, một trong những
- “attributes”: thuộc tính đã sửa đổi
- “characterData”: dữ liệu được sửa đổi, được sử dụng cho các nút văn bản,
- “childList”: các phần tử con được thêm / bớt,
- target- nơi xảy ra thay đổi: một phần tử cho “attributes” hoặc nút văn bản cho “characterData” hoặc một phần tử cho một “childList” đột biến,
- addedNodes/removedNodes – các nút đã được thêm / xóa,
- previousSibling/nextSibling – anh chị em trước đó và tiếp theo đối với các nút được thêm / loại bỏ,
- attributeName/attributeNamespace – tên / không gian tên (đối với XML) của thuộc tính đã thay đổi,
- oldValue- giá trị trước đó, chỉ dành cho các thay đổi thuộc tính hoặc văn bản, nếu tùy chọn tương ứng được đặt attributeOldValue/ characterDataOldValue.
Ví dụ, đây là <div>
một thuộc tính contentEditable. Thuộc tính đó cho phép chúng ta tập trung vào nó và chỉnh sửa.
<div contentEditable id="elem">Click and <b>edit</b>, please</div>
<script>
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(the changes)
});
// observe everything except attributes
observer.observe(elem, {
childList: true, // observe direct children
subtree: true, // and lower descendants too
characterDataOldValue: true // pass old data to callback
});
</script>
Nếu chúng ta chạy code này trong trình duyệt, sau đó tập trung vào phần đã cho < div > và thay đổi văn bản bên trong <b>edit</b>
, console.log sẽ hiển thị một đột biến:
mutationRecords = [{
type: "characterData",
oldValue: "edit",
target: <text node>,
// other properties empty
}];
Nếu chúng ta thực hiện các thao tác chỉnh sửa phức tạp hơn, chẳng hạn như xóa dấu <b>edit</b>
, sự kiện đột biến có thể chứa nhiều bản ghi đột biến:
mutationRecords = [{
type: "childList",
target: <div#elem>,
removedNodes: [<b>],
nextSibling: <text node>,
previousSibling: <text node>
// other properties empty
}, {
type: "characterData"
target: <text node>
// ...mutation details depend on how the browser handles such removal
// it may coalesce two adjacent text nodes "edit " and ", please" into one node
// or it may leave them separate text nodes
}];
Vì vậy, MutationObserver cho phép phản ứng trên bất kỳ thay đổi nào trong cây con DOM.
2. Sử dụng để tích hợp
Khi điều đó có thể hữu ích?
Hãy tưởng tượng tình huống khi bạn cần thêm tập lệnh của bên thứ ba có chứa chức năng hữu ích, nhưng cũng thực hiện điều gì đó không mong muốn, ví dụ: hiển thị quảng cáo <div class="ads">Unwanted ads</div>
Đương nhiên, tập lệnh của bên thứ ba không cung cấp cơ chế nào để xóa nó.
Bằng cách sử dụng MutationObserver, chúng ta có thể phát hiện khi phần tử không mong muốn xuất hiện trong DOM của chúng ta và loại bỏ nó.
Có những tình huống khác khi tập lệnh của bên thứ ba thêm nội dung nào đó vào tài liệu của chúng ta và chúng ta muốn phát hiện, khi điều đó xảy ra, để điều chỉnh trang của chúng ta, thay đổi kích thước động một thứ gì đó, v.v.
MutationObserver cho phép thực hiện điều này.
3. Sử dụng cho kiến trúc
Cũng có những tình huống MutationObserver tốt từ quan điểm kiến trúc.
Giả sử chúng ta đang tạo một trang web về lập trình. Đương nhiên, các bài báo và các tài liệu khác có thể chứa các đoạn mã nguồn.
Đoạn mã như vậy trong đánh dấu HTML trông giống như sau:
...
<pre class="language-javascript"><code>
// here's the code
let hello = "world";
</code></pre>
...
Ngoài ra, chúng ta sẽ sử dụng thư viện đánh dấu JavaScript trên trang web của chúng ta, ví dụ: Prism.js . Lệnh gọi Prism.highlightElem(pre) để kiểm tra nội dung của các phần tử pre đó và thêm vào chúng các thẻ và kiểu đặc biệt để tô sáng cú pháp màu, tương tự như những gì bạn thấy trong các ví dụ ở đây, tại trang này.
Khi nào chính xác để chạy phương pháp đánh dấu đó? Chúng ta có thể làm điều đó trong sự kiện DOMContentLoaded hoặc ở cuối trang. Tại thời điểm đó, chúng tôi đã sẵn sàng DOM của mình, có thể tìm kiếm các phần tử pre[class*=”language”] và gọi Prism.highlightElem chúng:
// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);
Mọi thứ đều đơn giản cho đến nay, phải không? Có những <pre>
đoạn code trong HTML, chúng ta đánh dấu chúng.
Bây giờ chúng ta hãy tiếp tục. Giả sử chúng ta sẽ tìm nạp động vật liệu từ một máy chủ. Chúng ta sẽ nghiên cứu các phương thức cho điều đó sau trong hướng dẫn . Hiện tại, điều quan trọng nhất là chúng ta tìm nạp một bài báo HTML từ một máy chủ web và hiển thị nó theo yêu cầu:
let article = /* fetch new content from server */
articleElem.innerHTML = article;
articleHTML mới có thể chứa các đoạn code. Chúng ta cần kêu gọi Prism.highlightElem, nếu không họ sẽ không được đánh dấu.
Gọi Prism.highlightElem một bài báo tải động ở đâu và khi nào?
Chúng ta có thể thêm lệnh gọi đó vào code tải một bài báo, 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 hãy tưởng tượng, chúng ta có nhiều nơi trong code nơi chúng ta tải nội dung: bài báo, câu đố, bài đăng trên diễn đàn. Chúng ta có cần đặt lệnh gọi đánh dấu ở khắp mọi nơi không? Điều đó không thuận tiện lắm, và cũng dễ quên.
Và điều gì sẽ xảy ra nếu nội dung được tải bởi mô-đun của bên thứ ba? Ví dụ: chúng ta có một diễn đàn do người khác viết, tải nội dung động và chúng tôi muốn thêm tô sáng cú pháp vào đó. Không ai thích vá các tập lệnh của bên thứ ba.
May mắn thay, có một lựa chọn khác.
Chúng ta có thể sử dụng MutationObserver để tự động phát hiện khi các đoạn code được chèn vào trang và đánh dấu chúng.
Vì vậy, chúng ta sẽ xử lý chức năng đánh dấu ở một nơi, giúp chúng ta không cần phải tích hợp nó.
3.1. Bản trình diễn đánh dấu động
Đây là ví dụ hoạt động.
Nếu bạn chạy code này, nó sẽ bắt đầu quan sát phần tử bên dưới và đánh dấu bất kỳ đoạn mã nào xuất hiện ở đó:
let observer = new MutationObserver(mutations => {
for(let mutation of mutations) {
// examine new nodes, is there anything to highlight?
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);
}
// or 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});
Ở đây, bên dưới, có một phần tử HTML và JavaScript tự động điền nó bằng cách sử dụng innerHTML.
Vui lòng chạy code trước (ở trên, quan sát phần tử đó), rồi chạy code bên dưới. Bạn sẽ thấy cách MutationObserver phát hiện và đánh dấu đoạn mã.
Một phần tử demo với id=”highlight-demo”, hãy chạy đoạn code trên để quan sát nó.
Đoạn mã sau sẽ điền vào code innerHTML, khiến code MutationObserver phản ứng và làm nổi bật nội dung của nó:
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>
`;
Bây giờ chúng ta có thể MutationObserver theo dõi tất cả các điểm nổi bật trong các phần tử được quan sát hoặc toàn bộ document. Chúng ta có thể thêm / bớt các đoạn code trong HTML mà không cần suy nghĩ về nó.
4. Các phương thức bổ sung
Có một phương pháp để ngừng quan sát nút:
- observer.disconnect() – ngừng quan sát.
Khi chúng ta dừng việc quan sát, có thể một số thay đổi chưa được người quan sát xử lý.
- observer.takeRecords() – nhận danh sách các bản ghi đột biến chưa được xử lý, những bản ghi đã xảy ra nhưng lệnh gọi lại không xử lý chúng.
Các phương thức này có thể được sử dụng cùng nhau, như sau:
// we'd like to stop tracking changes
observer.disconnect();
// handle unprocessed some mutations
let mutationRecords = observer.takeRecords();
...
Tương tác thu gom rác
Người quan sát sử dụng các tham chiếu yếu đến các nút trong nội bộ. Đó là: nếu một nút bị xóa khỏi DOM và trở nên không thể truy cập được, thì nút đó sẽ được thu gom.
Thực tế là một nút DOM được quan sát không ngăn cản việc thu thập rác.
5. Tóm lược
MutationObserver có thể phản ứng với những thay đổi trong DOM: thuộc tính, phần tử được thêm / bớt, nội dung văn bản.
Chúng ta có thể sử dụng nó để theo dõi các thay đổi được giới thiệu bởi các phần khác trong code của chúng ta, cũng như để tích hợp với các tập lệnh của bên thứ ba.
MutationObserver có thể theo dõi bất kỳ thay đổi nào. Tùy chọn cấu hình “những gì cần quan sát” được sử dụng để tối ưu hóa, không sử dụng tài nguyên cho các lệnh gọi lại không cần thiết.
Nguồn và tài liệu tiếng anh tham khảo:
Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!