ฉันจะได้รับแจ้งเมื่อมีการเพิ่มองค์ประกอบในหน้าได้อย่างไร


139

ฉันต้องการฟังก์ชั่นที่ฉันเลือกที่จะทำงานเมื่อเพิ่มองค์ประกอบ DOM ลงในหน้า นี่เป็นบริบทของส่วนขยายเบราว์เซอร์ดังนั้นหน้าเว็บจึงทำงานโดยอิสระจากฉันและฉันไม่สามารถแก้ไขแหล่งที่มาได้ ตัวเลือกของฉันที่นี่มีอะไรบ้าง

ฉันเดาว่าในทางทฤษฎีแล้วฉันสามารถใช้setInterval()เพื่อค้นหาการปรากฏตัวขององค์ประกอบอย่างต่อเนื่องและดำเนินการของฉันถ้าองค์ประกอบนั้นอยู่ที่นั่น แต่ฉันต้องการแนวทางที่ดีกว่า


คุณต้องตรวจสอบองค์ประกอบเฉพาะสคริปต์อื่นของคุณใส่ลงในหน้าหรือองค์ประกอบใด ๆ ที่เพิ่มเข้ามาไม่ว่าแหล่งที่มา?
Jose Faeti

1
คุณรู้หรือไม่ว่าฟังก์ชั่นเพิ่มองค์ประกอบในรหัสของคนอื่นถ้าเป็นเช่นนั้นคุณสามารถเขียนทับมันและเพิ่มหนึ่งบรรทัดพิเศษที่เรียกเหตุการณ์ที่กำหนดเอง?
jeffreydev

1
มีความเป็นไปได้ที่ซ้ำกันของjQuery DOM มีการเปลี่ยน listener หรือไม่?
apsillers

คำตอบ:


86

คำเตือน!

คำตอบนี้ล้าสมัยแล้ว DOM ระดับ 4 แนะนำMutationObserverซึ่งจะช่วยทดแทนเหตุการณ์การกลายพันธุ์ที่เลิกใช้แล้วอย่างมีประสิทธิภาพ ดูคำตอบนี้สำหรับคำตอบที่ดีกว่าคำตอบที่แสดงไว้ที่นี่ อย่างจริงจัง. อย่าสำรวจ DOM ทุก ๆ 100 มิลลิวินาที มันจะสิ้นเปลืองพลังงาน CPU และผู้ใช้ของคุณจะเกลียดคุณ

เนื่องจากเหตุการณ์การกลายพันธุ์ถูกเลิกใช้ในปี 2012 และคุณไม่สามารถควบคุมองค์ประกอบที่แทรกเนื่องจากจะถูกเพิ่มโดยรหัสของคนอื่นตัวเลือกเดียวของคุณคือการตรวจสอบอย่างต่อเนื่อง

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 );
}

เมื่อเรียกใช้ฟังก์ชันนี้แล้วจะเรียกใช้ทุก ๆ 100 มิลลิวินาทีซึ่งเป็น 1/10 (หนึ่งในสิบส่วน) ของวินาที หากคุณไม่ต้องการการสังเกตองค์ประกอบแบบเรียลไทม์ก็ควรจะเพียงพอ


1
อย่างไรคุณจะยุติการตั้งเวลาของคุณเมื่อเงื่อนไขตรงตามจริง? เช่นคุณพบองค์ประกอบที่โหลด x วินาทีในที่สุดลงในหน้าของคุณหรือไม่
klewis

1
@blachawk คุณต้องกำหนดค่าส่งคืน setTimeout ให้กับตัวแปรซึ่งคุณสามารถส่งเป็น paratemer ไปยัง clearTimeout () เพื่อล้างค่าได้ในภายหลัง
Jose Faeti

3
@blackhawk: ฉันเพิ่งเห็นคำตอบนั้นและ +1 มัน; แต่คุณควรระวังว่าคุณไม่จำเป็นต้องใช้ clearTimeout () ที่นี่เนื่องจาก setTimeout () ทำงานเพียงครั้งเดียว! หนึ่ง 'if-else' เพียงพอแล้ว: อย่าปล่อยให้ประมวลผลส่งผ่านบน setTimeout () เมื่อคุณพบองค์ประกอบที่แทรก
1111161171159459134

คู่มือปฏิบัติที่เป็นประโยชน์ต่อการใช้MutationObserver(): davidwalsh.name/mutationobserver-api
gfullam

4
มันสกปรก ไม่มี "เทียบเท่า" กับ AS3 Event.ADDED_TO_STAGEหรือไม่?
Bitterblue

45

คำตอบที่แท้จริงคือ "ใช้ผู้สังเกตการณ์การกลายพันธุ์" (ดังที่อธิบายไว้ในคำถามนี้: การพิจารณาว่าองค์ประกอบ HTML ถูกเพิ่มใน DOM แบบไดนามิก ) อย่างไรก็ตามการสนับสนุน (เฉพาะใน IE) มี จำกัด ( http://caniuse.com/mutationobserver ) .

ดังนั้นคำตอบที่แท้จริงคือ "ใช้ผู้สังเกตการณ์การกลายพันธุ์ .... ในที่สุด แต่ไปกับคำตอบของ Jose Faeti ในตอนนี้" :)


19

ระหว่างการคัดค้านเหตุการณ์การกลายพันธุ์และการเกิดขึ้นของMutationObserverวิธีการที่มีประสิทธิภาพที่จะได้รับการแจ้งเตือนเมื่อมีการเพิ่มองค์ประกอบเฉพาะใน DOM คือการใช้ประโยชน์จากเหตุการณ์ภาพเคลื่อนไหว CSS3ใช้ประโยชน์จากเหตุการณ์

หากต้องการอ้างอิงโพสต์บล็อก:

ตั้งค่าลำดับคีย์เฟรม CSS ที่กำหนดเป้าหมาย (ผ่านตัวเลือกตัวเลือก CSS) ของคุณไม่ว่าองค์ประกอบ DOM ใดที่คุณต้องการรับเหตุการณ์การแทรกโหนด DOM ฉันใช้คุณสมบัติ css ที่ค่อนข้างอ่อนโยนและใช้คลิปฉันใช้เค้าร่างสีเพื่อพยายามหลีกเลี่ยงการยุ่งกับสไตล์ของเพจที่ต้องการ - โค้ดครั้งหนึ่งเคยกำหนดเป้าหมายของคุณสมบัติคลิป แต่มันไม่สามารถเคลื่อนไหวได้ใน IE ในรุ่น 11 กล่าวว่าคุณสมบัติใด ๆ ที่สามารถสร้างภาพเคลื่อนไหวจะทำงานให้เลือกสิ่งที่คุณต้องการ

ต่อไปฉันเพิ่มผู้ฟังภาพเคลื่อนไหวเริ่มต้นกว้างที่ฉันใช้เป็นผู้รับมอบสิทธิ์ในการประมวลผลการแทรกโหนด เหตุการณ์ภาพเคลื่อนไหวมีคุณสมบัติที่เรียกว่า animationName ซึ่งจะบอกให้คุณทราบว่าลำดับคีย์เฟรมเริ่มต้นอย่างไรเมื่อภาพเคลื่อนไหว ตรวจสอบให้แน่ใจว่าคุณสมบัติ animationName เหมือนกับชื่อลำดับเฟรมเฟรมที่คุณเพิ่มไว้สำหรับการแทรกโหนดและคุณก็พร้อมใช้งาน


นี่เป็นโซลูชันที่มีประสิทธิภาพสูงสุดและมีคำตอบในคำถามที่ซ้ำกันที่กล่าวถึงห้องสมุดสำหรับสิ่งนี้
Dan Dascalescu

1
คำตอบที่ไม่ถูกต้อง
Gökhan Kurt

ระมัดระวัง หากความแม่นยำเป็นมิลลิวินาทีสำคัญวิธีนี้จะไม่แม่นยำ jsbin นี้แสดงให้เห็นว่ามีมากขึ้นกว่า 30ms ความแตกต่างระหว่างการเรียกกลับแบบอินไลน์และการใช้animationstart, jsbin.com/netuquralu/1/edit
Gajus

ฉันเห็นด้วยกับเคิร์ตนี่คือ underrated
Zabbu

13

คุณสามารถใช้livequeryปลั๊กอินสำหรับ jQuery คุณสามารถจัดเตรียมนิพจน์ตัวเลือกเช่น:

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

ทุกครั้งที่มีปุ่ม removeItemButtonคลาสข้อความจะปรากฏในแถบสถานะ

ในแง่ของประสิทธิภาพคุณอาจต้องการหลีกเลี่ยงปัญหานี้ แต่ในกรณีใด ๆ คุณสามารถใช้ประโยชน์จากปลั๊กอินแทนการสร้างตัวจัดการเหตุการณ์ของคุณเอง

คำตอบกลับมาอีกครั้ง

คำตอบข้างต้นมีไว้เพื่อตรวจจับว่ามีการเพิ่มรายการลงใน DOM เท่านั้นผ่านทางปลั๊กอิน

อย่างไรก็ตามเป็นไปได้มากว่าjQuery.on()แนวทางจะเหมาะสมกว่าตัวอย่างเช่น

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

jQuery.onหากคุณมีเนื้อหาแบบไดนามิกที่ควรจะตอบสนองต่อการคลิกเช่นนั้นดีที่สุดที่จะเหตุการณ์ผูกไว้กับภาชนะที่พ่อแม่ใช้


11

การทางพิเศษแห่งประเทศไทย 24 เมษายน 17 ฉันต้องการที่จะลดความซับซ้อนเล็กน้อยด้วยเวทมนตร์async/ awaitเพราะมันทำให้รวบรัดมากขึ้น:

ใช้ promisified-observable เดียวกัน:

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);
   })
} 

ฟังก์ชั่นการโทรของคุณสามารถทำได้ง่ายเพียง:

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)
    }
}

หากคุณต้องการเพิ่มการหมดเวลาคุณสามารถใช้Promise.raceรูปแบบง่าย ๆดังที่แสดงไว้ที่นี่ :

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)
    }
}

เป็นต้นฉบับ

คุณสามารถทำได้โดยไม่ต้องใช้ไลบรารี แต่คุณต้องใช้ ES6 บางอย่างดังนั้นควรรับรู้ถึงปัญหาความเข้ากันได้ (เช่นหากผู้ชมของคุณส่วนใหญ่เป็น Amish, luddite หรือแย่กว่านั้นคือผู้ใช้ IE8)

อันดับแรกเราจะใช้MutationObserver APIเพื่อสร้างวัตถุผู้สังเกตการณ์ เราจะห่อวัตถุนี้ไว้ในสัญญาและresolve()เมื่อมีการโทรกลับ (h / t davidwalshblog) david walsh บทความบล็อกเกี่ยวกับการกลายพันธุ์ :

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);
    })
} 

generator functionจากนั้นเราจะสร้าง หากคุณยังไม่ได้ใช้สิ่งเหล่านี้แสดงว่าคุณขาดหายไป แต่บทสรุปโดยย่อคือมันทำงานเหมือนฟังก์ชั่นซิงค์และเมื่อพบyield <Promise>นิพจน์มันจะรอโดยไม่มีการปิดกั้นเพื่อให้สัญญา สำเร็จแล้ว ( เครื่องกำเนิดไฟฟ้าทำมากกว่านี้ แต่นี่คือสิ่งที่เราสนใจที่นี่ )

// 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")
}

ส่วนที่ยุ่งยากเกี่ยวกับเครื่องกำเนิดไฟฟ้าคือพวกมันไม่ 'คืน' เหมือนฟังก์ชั่นปกติ ดังนั้นเราจะใช้ฟังก์ชันตัวช่วยเพื่อให้สามารถใช้ตัวสร้างเช่นฟังก์ชันทั่วไป (อีกครั้งh / t ถึง 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 );
            }
        }
    })();
}

จากนั้นที่จุดใดก่อนที่จะมีการกลายพันธุ์ DOM runGenerator(getMutation)คาดว่าอาจจะเกิดขึ้นเรียกเพียง

ตอนนี้คุณสามารถรวมการกลายพันธุ์ของ DOM ในโฟลว์การควบคุมแบบซิงโครนัส การแข่งขันนั้นเป็นอย่างไร


8

มีไลบรารีจาวาสคริปต์ที่มีชื่อว่า Arrive ซึ่งดูเหมือนว่าเป็นวิธีที่ดีในการเริ่มใช้ประโยชน์จากผู้สังเกตการณ์การกลายพันธุ์เมื่อการสนับสนุนเบราว์เซอร์กลายเป็นเรื่องธรรมดา

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


3

ลองใช้ปลั๊กอินนี้ที่ทำงานได้ดี - jquery.initialize

มันทำงานได้เหมือน exacly ฟังก์ชั่นแต่ละอย่างความแตกต่างคือต้องใช้ตัวเลือกที่คุณป้อนและดูรายการใหม่ที่เพิ่มเข้ามาในอนาคตที่ตรงกับตัวเลือกนี้และเริ่มต้นพวกมัน

เริ่มต้นดูเหมือนว่านี้

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

แต่ตอนนี้ถ้าจับคู่องค์ประกอบใหม่ .some-elementตัวเลือกการจะปรากฏในหน้านั้นจะเริ่มต้นได้ที่ instanty

วิธีการเพิ่มรายการใหม่ไม่สำคัญคุณไม่จำเป็นต้องสนใจเกี่ยวกับการโทรกลับ ฯลฯ

ดังนั้นหากคุณต้องการเพิ่มองค์ประกอบใหม่เช่น:

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

มันจะเริ่มต้นได้ทันที

ปลั๊กอินจะขึ้นอยู่กับ MutationObserver


0

วิธีการแก้ปัญหาจาวาสคริปต์บริสุทธิ์ (โดยไม่ต้อง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'));
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.