Mengenal MutationObserver API
Diterbitkan: 2022-03-10Di aplikasi web yang kompleks, perubahan DOM bisa sering terjadi. Akibatnya, ada kejadian di mana aplikasi Anda mungkin perlu merespons perubahan tertentu pada DOM.
Untuk beberapa waktu, cara yang diterima untuk mencari perubahan pada DOM adalah dengan menggunakan fitur yang disebut Mutation Events, yang sekarang tidak digunakan lagi. Pengganti yang disetujui W3C untuk Mutation Events adalah MutationObserver API, yang akan saya bahas secara rinci dalam artikel ini.
Sejumlah artikel dan referensi lama membahas mengapa fitur lama diganti, jadi saya tidak akan membahasnya secara rinci di sini (selain fakta bahwa saya tidak akan bisa melakukannya dengan adil). MutationObserver
API memiliki dukungan browser yang hampir lengkap, sehingga kami dapat menggunakannya dengan aman di sebagian besar — jika tidak semua — proyek, jika diperlukan.
Sintaks Dasar Untuk MutationObserver
MutationObserver
dapat digunakan dalam beberapa cara berbeda, yang akan saya bahas secara rinci di sisa artikel ini, tetapi sintaks dasar untuk MutationObserver
terlihat seperti ini:
let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);
Baris pertama membuat MutationObserver
baru menggunakan konstruktor MutationObserver()
. Argumen yang diteruskan ke konstruktor adalah fungsi panggilan balik yang akan dipanggil pada setiap perubahan DOM yang memenuhi syarat.
Cara untuk menentukan apa yang memenuhi syarat untuk pengamat tertentu adalah melalui baris terakhir dalam kode di atas. Pada baris itu, saya menggunakan metode observe()
dari MutationObserver
untuk mulai mengamati. Anda dapat membandingkan ini dengan sesuatu seperti addEventListener()
. Segera setelah Anda melampirkan pendengar, halaman akan 'mendengarkan' untuk acara yang ditentukan. Demikian pula, ketika Anda mulai mengamati, halaman akan mulai 'mengamati' untuk MutationObserver
yang ditentukan.
Metode observe()
membutuhkan dua argumen: Target , yang harus berupa simpul atau pohon simpul tempat mengamati perubahan; dan objek opsi , yang merupakan objek MutationObserverInit
yang memungkinkan Anda menentukan konfigurasi untuk pengamat.
Fitur dasar kunci terakhir dari MutationObserver
adalah metode disconnect()
. Ini memungkinkan Anda untuk berhenti mengamati perubahan yang ditentukan, dan terlihat seperti ini:
observer.disconnect();
Opsi Untuk Mengonfigurasi MutationObserver
Seperti disebutkan, metode observe()
dari MutationObserver
memerlukan argumen kedua yang menentukan opsi untuk mendeskripsikan MutationObserver
. Beginilah tampilan objek opsi dengan semua kemungkinan pasangan properti/nilai yang disertakan:
let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };
Saat menyiapkan opsi MutationObserver
, tidak perlu menyertakan semua baris ini. Saya menyertakan ini hanya untuk tujuan referensi, sehingga Anda dapat melihat opsi apa yang tersedia dan jenis nilai apa yang dapat diambil. Seperti yang Anda lihat, semua kecuali satu adalah Boolean.
Agar MutationObserver
berfungsi, setidaknya satu dari childList
, attributes
, atau characterData
perlu disetel ke true
, jika tidak, kesalahan akan muncul. Empat properti lainnya bekerja bersama dengan salah satu dari ketiganya (lebih lanjut tentang ini nanti).
Sejauh ini saya hanya mengabaikan sintaks untuk memberi Anda gambaran umum. Cara terbaik untuk mempertimbangkan cara kerja masing-masing fitur ini adalah dengan memberikan contoh kode dan demo langsung yang menggabungkan opsi yang berbeda. Jadi itulah yang akan saya lakukan untuk sisa artikel ini.
Mengamati Perubahan Pada Elemen Anak Menggunakan childList
MutationObserver
pertama dan paling sederhana yang dapat Anda mulai adalah yang mencari node anak dari node tertentu (biasanya elemen) untuk ditambahkan atau dihapus. Sebagai contoh saya, saya akan membuat daftar tidak berurutan di HTML saya, dan saya ingin tahu kapan simpul anak ditambahkan atau dihapus dari elemen daftar ini.
HTML untuk daftar terlihat seperti ini:
<ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>
JavaScript untuk MutationObserver
saya mencakup yang berikut:
let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);
Ini hanya bagian dari kode. Untuk singkatnya, saya menunjukkan bagian terpenting yang berhubungan dengan MutationObserver
API itu sendiri.
Perhatikan bagaimana saya mengulang argumen mutations
, yang merupakan objek MutationRecord
yang memiliki sejumlah properti berbeda. Dalam hal ini, saya membaca properti type
dan mencatat pesan yang menunjukkan bahwa browser telah mendeteksi mutasi yang memenuhi syarat. Juga, perhatikan bagaimana saya melewati elemen mList
(referensi ke daftar HTML saya) sebagai elemen yang ditargetkan (yaitu elemen yang ingin saya amati perubahannya).
- Lihat demo interaktif lengkap →
Gunakan tombol untuk memulai dan menghentikan MutationObserver
. Pesan log membantu memperjelas apa yang terjadi. Komentar dalam kode juga memberikan beberapa penjelasan.
Perhatikan beberapa poin penting di sini:
- Fungsi callback (yang saya beri nama
mCallback
, untuk mengilustrasikan bahwa Anda dapat menamainya apa pun yang Anda inginkan) akan diaktifkan setiap kali mutasi yang berhasil terdeteksi dan setelah metodeobserve()
dijalankan. - Dalam contoh saya, satu-satunya 'jenis' mutasi yang memenuhi syarat adalah
childList
, jadi masuk akal untuk mencari yang satu ini saat mengulang melalui MutationRecord. Mencari tipe lain dalam contoh ini tidak akan menghasilkan apa-apa (tipe lain akan digunakan dalam demo berikutnya). - Menggunakan
childList
, saya dapat menambah atau menghapus simpul teks dari elemen yang ditargetkan dan ini juga akan memenuhi syarat. Jadi tidak harus ada elemen yang ditambahkan atau dihilangkan. - Dalam contoh ini, hanya node turunan langsung yang memenuhi syarat. Nanti di artikel, saya akan menunjukkan kepada Anda bagaimana ini bisa diterapkan ke semua node anak, cucu, dan seterusnya.
Mengamati Perubahan Atribut Elemen
Jenis mutasi umum lainnya yang mungkin ingin Anda lacak adalah ketika atribut pada elemen tertentu berubah. Dalam demo interaktif berikutnya, saya akan mengamati perubahan atribut pada elemen paragraf.
let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
- Coba demonya →
Sekali lagi, saya telah menyingkat kode untuk kejelasan, tetapi bagian yang penting adalah:
- Objek
options
menggunakan propertiattributes
, disetel ketrue
untuk memberi tahuMutationObserver
bahwa saya ingin mencari perubahan pada atribut elemen yang ditargetkan. - Jenis mutasi yang saya uji dalam loop saya adalah
attributes
, satu-satunya yang memenuhi syarat dalam kasus ini. - Saya juga menggunakan properti
attributeName
dari objekmutation
, yang memungkinkan saya untuk mengetahui atribut mana yang diubah. - Saat saya memicu pengamat, saya meneruskan elemen paragraf dengan referensi, bersama dengan opsi.
Dalam contoh ini, tombol digunakan untuk mengganti nama kelas pada elemen HTML yang ditargetkan. Fungsi panggilan balik di pengamat mutasi dipicu setiap kali kelas ditambahkan atau dihapus.
Mengamati Perubahan Data Karakter
Perubahan lain yang mungkin ingin Anda cari di aplikasi Anda adalah mutasi ke data karakter; yaitu, perubahan ke node teks tertentu. Ini dilakukan dengan menyetel properti characterData
menjadi true
di objek options
. Berikut kodenya:
let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }
Perhatikan lagi type
yang dicari dalam fungsi callback adalah characterData
.
- Lihat demo langsung →
Dalam contoh ini, saya mencari perubahan pada simpul teks tertentu, yang saya targetkan melalui element.childNodes[0]
. Ini sedikit meretas tetapi akan berhasil untuk contoh ini. Teks dapat diedit pengguna melalui atribut contenteditable
pada elemen paragraf.
Tantangan Saat Mengamati Perubahan Data Karakter
Jika Anda telah mengutak-atik contenteditable
, maka Anda mungkin menyadari bahwa ada pintasan keyboard yang memungkinkan pengeditan teks kaya. Misalnya CTRL-B membuat teks menjadi tebal, CTRL-I membuat teks menjadi miring, dan lain sebagainya. Ini akan memecah simpul teks menjadi beberapa simpul teks, jadi Anda akan melihat MutationObserver
akan berhenti merespons kecuali Anda mengedit teks yang masih dianggap sebagai bagian dari simpul asli.
Saya juga harus menunjukkan bahwa jika Anda menghapus semua teks, MutationObserver
tidak akan lagi memicu panggilan balik. Saya berasumsi ini terjadi karena begitu simpul teks menghilang, elemen target tidak lagi ada. Untuk mengatasi hal ini, demo saya berhenti mengamati saat teks dihapus, meskipun hal-hal menjadi sedikit lengket saat Anda menggunakan pintasan teks kaya.
Tapi jangan khawatir, nanti di artikel ini, saya akan membahas cara yang lebih baik untuk menggunakan opsi characterData
tanpa harus berurusan dengan banyak quirks ini.
Mengamati Perubahan Atribut Tertentu
Sebelumnya saya menunjukkan cara mengamati perubahan atribut pada elemen tertentu. Dalam hal ini, meskipun demo memicu perubahan nama kelas, saya dapat mengubah atribut apa pun pada elemen yang ditentukan. Tetapi bagaimana jika saya ingin mengamati perubahan pada satu atau lebih atribut tertentu sambil mengabaikan yang lain?
Saya bisa melakukannya menggunakan properti attributeFilter
opsional di objek option
. Berikut ini contohnya:
let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
Seperti yang ditunjukkan di atas, properti attributeFilter
menerima larik atribut tertentu yang ingin saya pantau. Dalam contoh ini, MutationObserver
akan memicu panggilan balik setiap kali satu atau beberapa atribut hidden
, contenteditable
, atau data-par
diubah.
- Lihat demo langsung →
Sekali lagi saya menargetkan elemen paragraf tertentu. Perhatikan drop down pilih yang memilih atribut mana yang akan diubah. Atribut draggable
adalah satu-satunya yang tidak memenuhi syarat karena saya tidak menentukannya di opsi saya.
Perhatikan dalam kode bahwa saya kembali menggunakan properti attributeName
dari objek MutationRecord
untuk mencatat atribut mana yang diubah. Dan tentu saja, seperti demo lainnya, MutationObserver
tidak akan mulai memantau perubahan sampai tombol "mulai" diklik.
Satu hal lain yang harus saya tunjukkan di sini adalah bahwa saya tidak perlu menyetel nilai attributes
ke true
dalam kasus ini; itu tersirat karena attributesFilter
disetel ke true. Itu sebabnya objek opsi saya dapat terlihat sebagai berikut, dan itu akan berfungsi sama:
let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }
Di sisi lain, jika saya secara eksplisit menetapkan attributes
ke false
bersama dengan array attributeFilter
, itu tidak akan berfungsi karena nilai false
akan diutamakan dan opsi filter akan diabaikan.
Mengamati Perubahan Pada Node Dan Sub-Treenya
Sejauh ini ketika menyiapkan setiap MutationObserver
, saya hanya berurusan dengan elemen yang ditargetkan itu sendiri dan, dalam kasus childList
, anak-anak langsung dari elemen tersebut. Tetapi tentu saja mungkin ada kasus di mana saya mungkin ingin mengamati perubahan pada salah satu dari berikut ini:
- Sebuah elemen dan semua elemen anaknya;
- Satu atau lebih atribut pada elemen dan elemen turunannya;
- Semua node teks di dalam elemen.
Semua hal di atas dapat dicapai dengan menggunakan properti subtree
dari objek opsi.
childList Dengan subpohon
Pertama, mari kita cari perubahan pada simpul anak elemen, meskipun itu bukan turunan langsung. Saya dapat mengubah objek opsi saya agar terlihat seperti ini:
options = { childList: true, subtree: true }
Segala sesuatu yang lain dalam kode ini kurang lebih sama dengan contoh childList
sebelumnya, bersama dengan beberapa markup dan tombol tambahan.
- Lihat demo langsung →
Di sini ada dua daftar, satu bersarang di dalam yang lain. Saat MutationObserver
dimulai, panggilan balik akan memicu perubahan pada salah satu daftar. Tetapi jika saya mengubah properti subtree
kembali ke false
(default ketika tidak ada), panggilan balik tidak akan dijalankan ketika daftar bersarang dimodifikasi.
Atribut Dengan subpohon
Berikut contoh lain, kali ini menggunakan subtree
dengan attributes
dan attributeFilter
. Ini memungkinkan saya untuk mengamati perubahan atribut tidak hanya pada elemen target tetapi juga pada atribut elemen anak mana pun dari elemen target:
options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
- Lihat demo langsung →
Ini mirip dengan demo atribut sebelumnya, tetapi kali ini saya telah menyiapkan dua elemen pemilihan yang berbeda. Yang pertama memodifikasi atribut pada elemen paragraf yang ditargetkan sementara yang lain memodifikasi atribut pada elemen anak di dalam paragraf.
Sekali lagi, jika Anda menyetel opsi subtree
kembali ke false
(atau menghapusnya), tombol sakelar kedua tidak akan memicu panggilan balik MutationObserver
. Dan, tentu saja, saya bisa menghilangkan attributeFilter
sama sekali, dan MutationObserver
akan mencari perubahan pada atribut apa pun di subpohon daripada yang ditentukan.
characterData Dengan subpohon
Ingat di demo characterData
sebelumnya, ada beberapa masalah dengan node yang ditargetkan menghilang dan kemudian MutationObserver
tidak lagi berfungsi. Meskipun ada cara untuk menyiasatinya, lebih mudah untuk menargetkan elemen secara langsung daripada simpul teks, lalu gunakan properti subtree
untuk menentukan bahwa saya ingin semua data karakter di dalam elemen itu, tidak peduli seberapa dalam bersarangnya, untuk memicu panggilan balik MutationObserver
.
Opsi saya dalam hal ini akan terlihat seperti ini:
options = { characterData: true, subtree: true }
- Lihat demo langsung →
Setelah Anda memulai pengamat, coba gunakan CTRL-B dan CTRL-I untuk memformat teks yang dapat diedit. Anda akan melihat ini bekerja jauh lebih efektif daripada contoh characterData
sebelumnya. Dalam hal ini, node anak yang rusak tidak memengaruhi pengamat karena kami mengamati semua node di dalam node yang ditargetkan, bukan satu node teks.
Merekam Nilai Lama
Seringkali ketika mengamati perubahan pada DOM, Anda ingin mencatat nilai-nilai lama dan mungkin menyimpannya atau menggunakannya di tempat lain. Ini dapat dilakukan dengan menggunakan beberapa properti berbeda di objek options
.
atributNilai Lama
Pertama, mari kita coba logout nilai atribut lama setelah diubah. Begini tampilan opsi saya bersama dengan panggilan balik saya:
options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
- Lihat demo langsung →
Perhatikan penggunaan properti attributeName
dan oldValue
dari objek MutationRecord
. Coba demo dengan memasukkan nilai yang berbeda di bidang teks. Perhatikan bagaimana log diperbarui untuk mencerminkan nilai sebelumnya yang disimpan.
karakterDataNilai Lama
Demikian pula, inilah tampilan opsi saya jika saya ingin mencatat data karakter lama:
options = { characterData: true, subtree: true, characterDataOldValue: true }
- Lihat demo langsung →
Perhatikan pesan log menunjukkan nilai sebelumnya. Hal-hal menjadi sedikit miring ketika Anda menambahkan HTML melalui perintah teks kaya ke dalam campuran. Saya tidak yakin seperti apa perilaku yang benar dalam kasus itu, tetapi lebih mudah jika satu-satunya hal di dalam elemen adalah simpul teks tunggal.
Mencegat Mutasi Menggunakan takeRecords()
Metode lain dari objek MutationObserver
yang belum saya sebutkan adalah takeRecords()
. Metode ini memungkinkan Anda untuk sedikit banyak mencegat mutasi yang terdeteksi sebelum diproses oleh fungsi panggilan balik.
Saya dapat menggunakan fitur ini menggunakan baris seperti ini:
let myRecords = observer.takeRecords();
Ini menyimpan daftar perubahan DOM dalam variabel yang ditentukan. Dalam demo saya, saya menjalankan perintah ini segera setelah tombol yang mengubah DOM diklik. Perhatikan bahwa tombol mulai dan tambah/hapus tidak mencatat apa pun. Ini karena, seperti yang disebutkan, saya mencegat perubahan DOM sebelum diproses oleh panggilan balik.
Perhatikan, bagaimanapun, apa yang saya lakukan di pendengar acara yang menghentikan pengamat:
btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);
Seperti yang Anda lihat, setelah menghentikan pengamat menggunakan observer.disconnect()
, saya mengakses catatan mutasi yang dicegat dan saya mencatat elemen target serta jenis mutasi yang direkam. Jika saya telah mengamati beberapa jenis perubahan maka catatan yang disimpan akan memiliki lebih dari satu item di dalamnya, masing-masing dengan jenisnya sendiri.
Ketika catatan mutasi dicegat dengan cara ini dengan memanggil takeRecords()
, antrian mutasi yang biasanya dikirim ke fungsi panggilan balik dikosongkan. Jadi jika karena alasan tertentu Anda perlu mencegat catatan ini sebelum diproses, takeRecords()
akan berguna.
Mengamati Beberapa Perubahan Menggunakan Pengamat Tunggal
Perhatikan bahwa jika saya mencari mutasi pada dua node berbeda pada halaman, saya dapat melakukannya menggunakan pengamat yang sama. Ini berarti setelah saya memanggil konstruktor, saya dapat menjalankan metode observe()
untuk elemen sebanyak yang saya inginkan.
Jadi, setelah baris ini:
observer = new MutationObserver(mCallback);
Saya kemudian dapat memiliki beberapa panggilan observe()
dengan elemen berbeda sebagai argumen pertama:
observer.observe(mList, options); observer.observe(mList2, options);
- Lihat demo langsung →
Mulai pengamat, lalu coba tombol tambah/hapus untuk kedua daftar. Satu-satunya tangkapan di sini adalah jika Anda menekan salah satu tombol "berhenti", pengamat akan berhenti mengamati kedua daftar, bukan hanya yang ditargetkan.
Memindahkan Pohon Node yang Diamati
Satu hal terakhir yang akan saya tunjukkan adalah bahwa MutationObserver
akan terus mengamati perubahan pada node tertentu bahkan setelah node tersebut dihapus dari elemen induknya.
Misalnya, coba demo berikut:
- Lihat demo langsung →
Ini adalah contoh lain yang menggunakan childList
untuk memantau perubahan pada elemen turunan dari elemen target. Perhatikan tombol yang memutuskan sub-daftar, yang sedang diamati. Klik tombol "Mulai ...", lalu klik tombol "Pindahkan ..." untuk memindahkan daftar bersarang. Bahkan setelah daftar dihapus dari induknya, MutationObserver
terus mengamati perubahan yang ditentukan. Bukan kejutan besar bahwa ini terjadi, tapi itu sesuatu yang perlu diingat.
Kesimpulan
Itu mencakup hampir semua fitur utama dari MutationObserver
API. Saya harap penyelaman dalam ini bermanfaat bagi Anda untuk terbiasa dengan standar ini. Seperti yang disebutkan, dukungan browser kuat dan Anda dapat membaca lebih lanjut tentang API ini di halaman MDN.
Saya telah memasukkan semua demo untuk artikel ini ke dalam koleksi CodePen, jika Anda ingin memiliki tempat yang mudah untuk bermain-main dengan demo.