Untuk memastikan pengendali klik terus berfungsi selepas menggantikan kandungan DOM sesuatu paparan, pasangkan satu pendengar pada elemen induk yang kekal selepas kemas kini. Panduan ini menunjukkan cara melaksanakan dan menyemak corak tersebut dalam Safari pada Mac menggunakan pemilih jubin sintetik yang ringkas.

Tugas ini khusus: bina semula kandungan paparan JavaScript sambil memastikan butangnya responsif dan pilihan kekal. Anda memerlukan pengetahuan asas JavaScript dan pelayar Safari terkini. Contoh ini menggunakan API pelayar standard dan tidak memerlukan rangka kerja atau pemasangan pakej.

Mengapa klik berhenti berfungsi selepas penggantian DOM?

Pendengar yang didaftarkan dengan addEventListener() terikat pada sasaran peristiwa tempat pendengar itu didaftarkan. Menetapkan innerHTML pada bekas akan menghuraikan markup yang diberikan dan menggantikan keturunannya. Oleh itu, butang yang baru dicipta tidak mewarisi pendengar yang dipasang pada butang sebelumnya, walaupun ID dan labelnya sepadan. Lihat rujukan innerHTML MDN dan model pendengar peristiwa DOM Standard.

Perbezaan yang penting ialah antara menggantikan kandungan bekas dengan menggantikan bekas itu sendiri. Jika kod anda mengekalkan bekas dan hanya membina semula anak-anaknya, pendengar pada bekas itu boleh kekal. Jika paparan luar turut menggantikan bekas tersebut, anda mesti memilih leluhur yang kekal atau memasang pendengar semasa pemulaan bekas baharu.

Bagi peristiwa klik biasa yang menggelembung, delegasi peristiwa membolehkan induk mengendalikan interaksi yang bermula daripada keturunannya. Daripada mengikat setiap butang gantian, ikat induk yang stabil dan kenal pasti butang yang berkenaan apabila peristiwa tiba. Panduan penggelembungan peristiwa MDN menerangkan corak ini.

Di manakah pendengar patut diletakkan?

Mulakan dengan mengenal pasti sempadan DOM tepat yang digantikan oleh operasi pemaparan anda. Kemudian pilih induk paling kecil yang sesuai di luar sempadan itu. Panduan keputusan berikut menerapkan tingkah laku penggantian dan pendengar yang didokumenkan pada corak kemas kini lazim. MDN: replaceChildren(), DOM Standard: EventTarget.

Kemas kini anda Peletakan pendengar Keperluan kitar hayat
Bina semula anak-anak akar paparan sedia ada Delegasikan klik daripada akar itu Daftarkan sekali apabila akar dicipta
Gantikan akar paparan juga Delegasikan daripada bekas luar yang kekal, atau ikat akar baharu Bersihkan pendaftaran paparan sebelumnya
Bina butang yang memerlukan pengendali individu Ikat setiap butang baharu semasa pembinaan Selesaikan pengikatan sebelum kawalan baharu tersedia
Muat semula seluruh dokumen Mulakan pendengar dalam dokumen baharu Jalankan semula pemulaan halaman seperti biasa

Untuk contoh di bawah, strukturnya kekal sebagai akar paparan yang mengandungi tajuk, status, set pilihan jubin tersusun dan kawalan muat semula. Setiap pemaparan membina semula anak-anak itu dalam susunan yang sama. Hanya akar paparan, pendengar dan keadaan JavaScript yang kekal.

Bina paparan lengkap yang selamat daripada penggantian dalam Safari

Mula-mula sediakan tab pelayar sementara. Dalam Safari pada Mac anda, buka Settings…, pilih Advanced, dan aktifkan Show features for web developers. Buka Web Inspector menggunakan Develop → Show Web Inspector, atau tekan Option-Command-I. Ini ialah langkah yang didokumenkan dalam panduan WebKit untuk mengaktifkan Web Inspector.

Pergi ke about:blank, buka Web Inspector dan pilih Console. Tampal keseluruhan program di bawah pada gesaannya, kemudian jalankannya. WebKit mendokumenkan bahawa Prompt Konsol menilai JavaScript dalam konteks pelaksanaan terpilih bagi halaman yang diperiksa. Gunakan tab kosong supaya demonstrasi ini kekal terpisah daripada aplikasi anda.

Ini ialah program pelayar yang lengkap: program ini mencipta DOM sendiri, mentakrifkan data sampel, memasang paparan dan menyediakan kaedah pembersihan. Program ini menggantikan anak-anak paparan menggunakan replaceChildren(), yang menerima objek nod dan menggantikan set anak yang sedia ada. Rujukan kaedah MDN menerangkan operasi itu.

(() => {
  window.domClickDemo?.destroy();

  const root = document.createElement("section");
  root.id = "dom-click-demo";
  const controller = new AbortController();
  const items = [
    { id: "a", label: "Tile A" },
    { id: "b", label: "Tile B" },
    { id: "c", label: "Tile C" }
  ];
  const state = { selected: "None", refreshes: 0 };

  function makeButton(action, label, id) {
    const button = document.createElement("button");
    button.type = "button";
    button.dataset.action = action;
    if (id !== undefined) button.dataset.id = id;
    const caption = document.createElement("span");
    caption.textContent = label;
    button.append(caption);
    return button;
  }

  function render() {
    const heading = document.createElement("h2");
    heading.textContent = "Tile picker";
    const status = document.createElement("p");
    status.textContent =
      `Selected: ${state.selected}; refreshes: ${state.refreshes}`;
    const list = document.createElement("ul");
    for (const item of items) {
      const row = document.createElement("li");
      row.append(makeButton("select", item.label, item.id));
      list.append(row);
    }
    const refresh = makeButton("refresh", "Replace view content");
    root.replaceChildren(heading, status, list, refresh);
  }

  function handleClick(event) {
    if (!(event.target instanceof Element)) return;
    const button = event.target.closest("button[data-action]");
    if (!button || !root.contains(button) || button.disabled) return;

    if (button.dataset.action === "select") {
      const item = items.find(item => item.id === button.dataset.id);
      if (!item) return;
      state.selected = item.label;
    } else if (button.dataset.action === "refresh") {
      state.refreshes += 1;
    } else {
      return;
    }
    render();
  }

  root.addEventListener("click", handleClick, {
    signal: controller.signal
  });
  render();
  document.body.append(root);

  window.domClickDemo = {
    root,
    destroy() {
      controller.abort();
      root.remove();
    }
  };
})();

Pendengar didaftarkan di luar render(). Setiap pilihan dan muat semula membina butang baharu, tetapi semua butang itu kekal di bawah akar asal. Objek keadaan juga berada di luar subpokok yang dipaparkan, jadi penggantian perenggan status tidak menetapkan semula jubin yang dipilih atau kiraan muat semula.

Kapsyen bersarang itu disengajakan. Klik pada teks butang boleh menyasarkan elemen anaknya. closest("button[data-action]") mencari kawalan yang sepadan pada sasaran dan leluhurnya, sekali gus mengelakkan pengendali yang hanya berfungsi apabila butang itu sendiri menjadi sasaran peristiwa. Rujukan closest() MDN menerangkan carian itu serta hasil null apabila tiada yang sepadan.

Semakan keterangkuman memastikan kawalan sepadan itu kekal dalam sempadan paparan ini. DOM Standard mentakrifkan contains() berdasarkan sama ada nod yang diberikan ialah nod itu sendiri atau salah satu keturunannya. DOM Standard: Node.contains().

Bezakan pemulaan, pemaparan dan pelupusan

Contoh ini mempunyai tiga tanggungjawab yang jelas. Pemulaan mencipta akar dan mendaftarkan pendengarnya. Pemaparan mengemas kini anak-anak akar berdasarkan keadaan. Pelupusan mengalih keluar pendengar dan akar. Pembahagian ini ialah kontrak kitar hayat pelaksanaan; kekalkan tanggungjawab yang sepadan apabila menyesuaikannya untuk aplikasi anda.

Pendengar menerima AbortSignal, dan destroy() membatalkan pengawalnya. Pembatalan mengalih keluar pendaftaran yang berkaitan, seperti yang didokumenkan dalam rujukan addEventListener() MDN. Menjalankan semula keseluruhan program terlebih dahulu melupuskan demonstrasi sebelumnya, sekali gus mengelakkan beberapa paparan demo daripada terkumpul.

Jangan letakkan pendengar tanpa nama yang baharu dicipta dalam setiap pemaparan. Ungkapan fungsi tanpa nama yang diulang mencipta objek fungsi yang berbeza, maka pendaftarannya boleh terkumpul pada akar yang kekal. Menggunakan semula panggil balik, jenis peristiwa dan tetapan tangkapan yang sama mengelakkan pendaftaran pendua, tetapi mengekalkan pengikatan dalam pemulaan menjadikan pemilikan lebih jelas. MDN: addEventListener(), DOM Standard: menambah pendengar.

Semak penggantian berulang, pilihan dan pembersihan

Gunakan urutan ini untuk menguji contoh. Hasil yang dijangka ini berdasarkan kemas kini keadaan yang dinyatakan secara jelas dalam program; hasil ini bukan dakwaan bahawa ujian Safari telah selesai.

  1. Klik Tile B. Status sepatutnya memaparkan Selected: Tile B; refreshes: 0.
  2. Klik Replace view content tiga kali. Status sepatutnya memaparkan Selected: Tile B; refreshes: 3.
  3. Klik teks Tile C. Status sepatutnya berubah kepada Selected: Tile C; refreshes: 3.
  4. Klik tajuk atau perenggan status. Tiada nilai sepatutnya berubah.
  5. Jalankan window.domClickDemo.destroy() dalam Konsol. Demonstrasi sepatutnya hilang.
  6. Jalankan semula keseluruhan program. Satu paparan baharu sepatutnya muncul dengan Selected: None; refreshes: 0.

Tiga semakan pertama meliputi penggantian, keadaan yang dikekalkan dan sasaran klik bersarang. Semakan keempat menguji penapisan tindakan. Dua semakan terakhir menguji laluan pelupusan dan pemulaan. Secara bersama, semakan ini menguji kitar hayat sebenar, bukan sekadar mengesahkan bahawa butang awal berfungsi.

Sintaks JavaScript program ini disemak dengan Node, dan semakan EventTarget deterministik yang berasingan mengesahkan penyahgandaan panggil balik serta pengalihan keluar melalui isyarat pembatalan. Pemaparan DOM pelayar dan interaksi Safari tidak dijalankan dalam semakan itu; gunakan urutan di atas untuk mengesahkannya pada Mac anda.

Bagaimana jika pengendali masih gagal dalam aplikasi anda?

Siasat titik pertama yang menunjukkan tingkah laku berbeza. Pastikan skop contoh kekal kecil sambil menyemak sempadan penggantian, laluan peristiwa dan kemas kini keadaan dalam aplikasi anda.

Gejala Perkara pertama untuk disemak Pembetulan yang sesuai
Klik berfungsi sehingga paparan luar dimuat semula Adakah akar pendengar turut digantikan? Ikat akar baharu semasa pemulaan atau pilih induk yang kekal
Klik diterima, tetapi tiada tindakan dijalankan Adakah kawalan baharu masih sepadan dengan pemilih dan nilai tindakan? Kekalkan pemilih kawalan dan kontrak tindakannya
Klik pada teks butang memberikan hasil berbeza Adakah pengendali hanya menyemak sasaran tepat? Cari kawalan menggunakan closest()
Satu klik mengemas kini keadaan beberapa kali Adakah beberapa pendaftaran atau pengendali langsung dan delegasi aktif serentak? Tetapkan satu laluan pemulaan untuk mengendalikan tindakan itu
Pengendali berjalan, tetapi paparan ditetapkan semula Adakah keadaan dicipta semula dalam pemaparan? Simpan keadaan yang diperlukan di luar subpokok yang digantikan

Periksa juga perambatan. Pengendali keturunan yang memanggil stopPropagation() boleh menghalang peristiwa yang menggelembung daripada sampai kepada pendengar delegasi anda. MDN menerangkan penghentian perambatan dan delegasi peristiwa. Semak sebab perambatan dihentikan sebelum menukar fasa pendengar, kerana perubahan fasa turut mengubah susunan pelaksanaan.

Jika anda perlu menggunakan pendengar butang langsung, pasangkannya setiap kali anda membina butang gantian. Pertanyaan dan proses pengikatan sekali sahaja tidak dapat memulakan nod yang dicipta oleh pemaparan kemudian: pendaftaran terikat pada sasaran peristiwanya. Begitu juga, delegasi tidak dapat mengekalkan pendengar pada akar yang dibuang oleh pemapar anda. Sempadan ini berpunca daripada model pendengar DOM Standard.

Untuk tugas ini, susunan yang kekal teguh ialah akar paparan yang stabil, satu pendengar delegasi, keadaan yang disimpan di luar anak-anak yang dipaparkan, dan laluan pembersihan yang jelas. Kenal pasti nod yang kekal selepas setiap kemas kini, letakkan pendengar sewajarnya dan sahkan beberapa penggantian berturut-turut sebelum menyepadukan perubahan itu ke dalam paparan yang lebih besar.

Sumber