Bagaimana saya bisa diberi tahu ketika sebuah elemen ditambahkan ke halaman?

142

Saya ingin fungsi yang saya pilih berjalan saat elemen DOM ditambahkan ke halaman. Ini dalam konteks ekstensi browser, jadi halaman web berjalan secara independen dari saya dan saya tidak dapat mengubah sumbernya. Apa saja pilihan saya di sini?

Saya rasa, secara teori, saya dapat menggunakan setInterval()untuk terus mencari keberadaan elemen dan melakukan tindakan saya jika elemen tersebut ada, tetapi saya membutuhkan pendekatan yang lebih baik.

Kevin Burke
sumber
Apakah Anda harus memeriksa elemen tertentu yang dimasukkan skrip lain milik Anda ke halaman atau elemen apa pun yang ditambahkan apa pun sumbernya?
Jose Faeti
1
tahukah Anda fungsi apa yang menambahkan elemen dalam kode orang lain, jika demikian Anda dapat menimpanya dan menambahkan satu baris tambahan yang memicu peristiwa khusus?
jeffreydev
1
kemungkinan duplikat Apakah ada pendengar perubahan jQuery DOM?
apsillers

Jawaban:

87

Peringatan!

Jawaban ini sekarang sudah ketinggalan zaman. DOM Level 4 memperkenalkan MutationObserver , memberikan pengganti yang efektif untuk peristiwa mutasi yang tidak digunakan lagi. Lihat jawaban ini untuk solusi yang lebih baik daripada yang disajikan di sini. Sungguh. Jangan polling DOM setiap 100 milidetik; itu akan menghabiskan daya CPU dan pengguna Anda akan membenci Anda.

Karena peristiwa mutasi tidak digunakan lagi pada tahun 2012, dan Anda tidak memiliki kontrol atas elemen yang disisipkan karena ditambahkan oleh kode orang lain, satu-satunya pilihan Anda adalah terus memeriksanya.

function checkDOMChange()
{
    // check for any new element being inserted here,
    // or a particular node being modified

    // call the function again after 100 milliseconds
    setTimeout( checkDOMChange, 100 );
}

Setelah dipanggil, fungsi ini akan berjalan setiap 100 milidetik, yaitu 1/10 (sepersepuluh) detik. Kecuali jika Anda memerlukan observasi elemen secara real-time, itu sudah cukup.

Jose Faeti
sumber
1
jose, bagaimana Anda mengakhiri settimeout Anda ketika kondisi benar-benar terpenuhi? yaitu, Anda menemukan elemen yang akhirnya dimuat x detik kemudian ke laman Anda?
klewis
1
@blachawk Anda perlu menetapkan nilai kembalian setTimeout ke variabel, yang nanti dapat Anda berikan sebagai penerjun payung ke clearTimeout () untuk menghapusnya.
Jose Faeti
3
@blackhawk: Saya baru saja melihat jawaban itu dan memberi +1; tetapi Anda akan sadar bahwa Anda sebenarnya tidak perlu menggunakan clearTimeout () di sini, karena setTimeout () hanya berjalan sekali! Satu 'if-else' sudah cukup: jangan biarkan eksekusi diteruskan pada setTimeout () setelah Anda menemukan elemen yang disisipkan.
1111161171159459134
Sebuah panduan yang berguna praktis untuk menggunakan MutationObserver(): davidwalsh.name/mutationobserver-api
gfullam
4
Itu kotor. Apakah benar-benar tidak ada yang "setara" dengan AS3 Event.ADDED_TO_STAGE?
Bitterblue
19

Antara penghentian peristiwa mutasi dan kemunculannya MutationObserver, cara efektif untuk diberi tahu saat elemen tertentu ditambahkan ke DOM adalah dengan mengeksploitasi peristiwa animasi CSS3 .

Mengutip postingan blog:

Siapkan urutan bingkai utama CSS yang menargetkan (melalui pemilih CSS pilihan Anda) elemen DOM apa pun yang Anda inginkan untuk menerima peristiwa penyisipan simpul DOM. Saya menggunakan properti css yang relatif jinak dan sedikit digunakan, klip saya menggunakan garis-warna dalam upaya untuk menghindari mengotak-atik gaya halaman yang dimaksudkan - kode pernah menargetkan properti klip, tetapi tidak lagi dapat dianimasikan di IE pada versi 11. Itu mengatakan, properti apa pun yang dapat dianimasikan akan berfungsi, pilih mana yang Anda suka.

Selanjutnya saya menambahkan pendengar animationstart seluruh dokumen yang saya gunakan sebagai delegasi untuk memproses penyisipan node. Acara animasi memiliki properti bernama animationName di atasnya yang memberi tahu Anda urutan bingkai utama mana yang memulai animasi. Pastikan saja properti animationName sama dengan nama urutan keyframe yang Anda tambahkan untuk penyisipan node dan Anda siap melakukannya.

lelucon
sumber
Ini adalah solusi dengan kinerja tertinggi, dan ada jawaban dalam pertanyaan duplikat yang menyebutkan perpustakaan untuk ini.
Dan Dascalescu
1
Jawaban yang diremehkan.
Gökhan Kurt
Cermat. Jika presisi milidetik itu penting, pendekatan ini jauh dari akurat. Jsbin ini menunjukkan bahwa ada lebih dari 30 md perbedaan antara panggilan balik sebaris dan menggunakan animationstart, jsbin.com/netuquralu/1/edit .
Gajus
Saya setuju dengan kurt, ini diremehkan.
Zabbu
13

Anda dapat menggunakan livequeryplugin untuk jQuery. Anda dapat memberikan ekspresi pemilih seperti:

$("input[type=button].removeItemButton").livequery(function () {
    $("#statusBar").text('You may now remove items.');
});

Setiap kali tombol a removeItemButton kelas ditambahkan, pesan muncul di bilah status.

Dalam hal efisiensi, Anda mungkin ingin menghindari ini, tetapi dalam hal apa pun Anda dapat memanfaatkan plugin daripada membuat penangan acara Anda sendiri.

Jawaban yang ditinjau kembali

Jawaban di atas hanya untuk mendeteksi bahwa suatu item telah ditambahkan ke DOM melalui plugin.

Namun, kemungkinan besar, jQuery.on()pendekatan akan lebih tepat, misalnya:

$("#myParentContainer").on('click', '.removeItemButton', function(){
          alert($(this).text() + ' has been removed');
});

Misalnya, jika Anda memiliki konten dinamis yang harus merespons klik, sebaiknya ikat peristiwa ke penampung induk menggunakan jQuery.on.

Nol Gangguan
sumber
11

ETA 24 Apr 17 Saya ingin menyederhanakan ini sedikit dengan beberapa async/await magic, karena membuatnya jauh lebih ringkas:

Menggunakan promisified-observable yang sama:

const startObservable = (domNode) => {
  var targetNode = domNode;

  var observerConfig = {
    attributes: true,
    childList: true,
    characterData: true
  };

  return new Promise((resolve) => {
      var observer = new MutationObserver(function (mutations) {
         // For the sake of...observation...let's output the mutation to console to see how this all works
         mutations.forEach(function (mutation) {
             console.log(mutation.type);
         });
         resolve(mutations)
     });
     observer.observe(targetNode, observerConfig);
   })
} 

Fungsi panggilan Anda bisa sesederhana:

const waitForMutation = async () => {
    const button = document.querySelector('.some-button')
    if (button !== null) button.click()
    try {
      const results = await startObservable(someDomNode)
      return results
    } catch (err) { 
      console.error(err)
    }
}

Jika Anda ingin menambahkan batas waktu, Anda dapat menggunakan Promise.racepola sederhana seperti yang ditunjukkan di sini :

const waitForMutation = async (timeout = 5000 /*in ms*/) => {
    const button = document.querySelector('.some-button')
    if (button !== null) button.click()
    try {

      const results = await Promise.race([
          startObservable(someDomNode),
          // this will throw after the timeout, skipping 
          // the return & going to the catch block
          new Promise((resolve, reject) => setTimeout(
             reject, 
             timeout, 
             new Error('timed out waiting for mutation')
          )
       ])
      return results
    } catch (err) { 
      console.error(err)
    }
}

Asli

Anda dapat melakukan ini tanpa perpustakaan, tetapi Anda harus menggunakan beberapa barang ES6, jadi waspadalah terhadap masalah kompatibilitas (yaitu, jika audiens Anda sebagian besar adalah Amish, luddite atau, lebih buruk, pengguna IE8)

Pertama, kita akan menggunakan API MutationObserver untuk membuat objek pengamat. Kami akan membungkus objek ini dalam sebuah promise, dan resolve()saat callback diaktifkan (h / t davidwalshblog) artikel blog david walsh tentang mutasi :

const startObservable = (domNode) => {
    var targetNode = domNode;

    var observerConfig = {
        attributes: true,
        childList: true,
        characterData: true
    };

    return new Promise((resolve) => {
        var observer = new MutationObserver(function (mutations) {
            // For the sake of...observation...let's output the mutation to console to see how this all works
            mutations.forEach(function (mutation) {
                console.log(mutation.type);
            });
            resolve(mutations)
        });
        observer.observe(targetNode, observerConfig);
    })
} 

Kemudian, kami akan membuat file generator function. Jika Anda belum menggunakan ini, maka Anda ketinggalan - tetapi sinopsis singkatnya adalah: ini berjalan seperti fungsi sinkronisasi, dan ketika menemukan yield <Promise>ekspresi, itu menunggu dengan cara yang tidak menghalangi janji itu akan terjadi. terpenuhi ( Generator melakukan lebih dari ini, tetapi inilah yang kami minati di sini ).

// we'll declare our DOM node here, too
let targ = document.querySelector('#domNodeToWatch')

function* getMutation() {
    console.log("Starting")
    var mutations = yield startObservable(targ)
    console.log("done")
}

Bagian rumit tentang generator adalah mereka tidak 'mengembalikan' seperti fungsi normal. Jadi, kita akan menggunakan fungsi pembantu agar bisa menggunakan generator seperti fungsi biasa. (sekali lagi, h / t ke dwb )

function runGenerator(g) {
    var it = g(), ret;

    // asynchronously iterate over generator
    (function iterate(val){
        ret = it.next( val );

        if (!ret.done) {
            // poor man's "is it a promise?" test
            if ("then" in ret.value) {
                // wait on the promise
                ret.value.then( iterate );
            }
            // immediate value: just send right back in
            else {
                // avoid synchronous recursion
                setTimeout( function(){
                    iterate( ret.value );
                }, 0 );
            }
        }
    })();
}

Kemudian, pada titik mana pun sebelum mutasi DOM yang diharapkan mungkin terjadi, jalankan saja runGenerator(getMutation) .

Sekarang Anda dapat mengintegrasikan mutasi DOM ke dalam aliran kontrol gaya sinkron. Bagaimana kalau itu.

Brandon
sumber
8

Ada pustaka javascript yang menjanjikan bernama Tiba yang terlihat seperti cara yang bagus untuk mulai memanfaatkan pengamat mutasi begitu dukungan browser menjadi hal yang biasa.

https://github.com/uzairfarooq/arrive/

Dave Kiss
sumber
3

Lihat plugin ini yang benar-benar melakukannya - jquery.initialize

Ini bekerja persis seperti fungsi .each, perbedaannya adalah dibutuhkan pemilih yang Anda masukkan dan perhatikan item baru yang ditambahkan di masa mendatang yang cocok dengan pemilih ini dan menginisialisasi mereka

Inisialisasi terlihat seperti ini

$(".some-element").initialize( function(){
    $(this).css("color", "blue");
});

Tetapi sekarang jika .some-elementpemilih pencocokan elemen baru akan muncul di halaman, itu akan diinisialisasi secara instan.

Cara item baru ditambahkan tidak penting, Anda tidak perlu peduli dengan callback, dll.

Jadi jika Anda menambahkan elemen baru seperti:

$("<div/>").addClass('some-element').appendTo("body"); //new element will have blue color!

itu akan langsung diinisialisasi.

Plugin didasarkan pada MutationObserver

pie6k.dll
sumber
0

Solusi javascript murni (tanpa jQuery):

const SEARCH_DELAY = 100; // in ms

// it may run indefinitely. TODO: make it cancellable, using Promise's `reject`
function waitForElementToBeAdded(cssSelector) {
  return new Promise((resolve) => {
    const interval = setInterval(() => {
      if (element = document.querySelector(cssSelector)) {
        clearInterval(interval);
        resolve(element);
      }
    }, SEARCH_DELAY);
  });
}

console.log(await waitForElementToBeAdded('#main'));
vedant
sumber