最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - MutationObserver's DOM context is lost because it fires too late - Stack Overflow

programmeradmin3浏览0评论

Simplified version of my code:

<div id="d">text<br><hr>text</div>

<script>
    // Called when DOM changes.
    function mutationCallback(mutations) {
        // assert(mutations.length === 3);
        var insertImg = mutations[0];
        console.log(insertImg.previousSibling.parentNode);  // Null!
        console.log(insertImg.nextSibling.parentNode); // Null!
        // Can't determine where img was inserted!
    }
  
    // Setup
    var div = document.getElementById('d');
    var br = div.childNodes[1];
    var hr = div.childNodes[2];
    var observer = new MutationObserver(mutationCallback);
    observer.observe(div, {childList: true, subtree: true});

    // Trigger DOM Changes.
    var img = document.createElement('img');
    div.insertBefore(img, hr);
    div.removeChild(hr);
    div.removeChild(br); // mutationCallback() is first called after this line.
</script>

Simplified version of my code:

<div id="d">text<br><hr>text</div>

<script>
    // Called when DOM changes.
    function mutationCallback(mutations) {
        // assert(mutations.length === 3);
        var insertImg = mutations[0];
        console.log(insertImg.previousSibling.parentNode);  // Null!
        console.log(insertImg.nextSibling.parentNode); // Null!
        // Can't determine where img was inserted!
    }
  
    // Setup
    var div = document.getElementById('d');
    var br = div.childNodes[1];
    var hr = div.childNodes[2];
    var observer = new MutationObserver(mutationCallback);
    observer.observe(div, {childList: true, subtree: true});

    // Trigger DOM Changes.
    var img = document.createElement('img');
    div.insertBefore(img, hr);
    div.removeChild(hr);
    div.removeChild(br); // mutationCallback() is first called after this line.
</script>

I am using Mutation Observers to capture DOM changes, to update one document instance when another changes. Because the mutation observer function is not called until after <img>'s previous and next sibling are removed, the mutationCallback function can't tell where it was inserted. Reproduced in Chrome, FF, and IE11.

An alternative is to traverse the whole document to find changes, but that would negate the performance advantage of using Mutation Observers.

Share Improve this question edited Mar 24, 2018 at 0:55 EricP asked Mar 13, 2018 at 18:22 EricPEricP 3,4393 gold badges36 silver badges47 bronze badges 9
  • 2 Why do you want the .parentNode to begin with? mutations[0].target gives the the p and mutations[0].removedNodes[0] gives you the x, and then separately, since it is a separate mutations, mutations[1].target gives to div and mutations[1].removedNodes[0] gives you p. That information is enough to reconstruct what happened. – loganfsmyth Commented Mar 13, 2018 at 18:46
  • I'm cloning changes from one document to another. The operations in the first document could occur in any order. So I can't simply hardcode mutations[1].target into my solution. – EricP Commented Mar 13, 2018 at 18:48
  • 1 Totally, but I'm saying you'd need to fully replay each item in the mutations array onto your other document, and each individual mutations item tells you which node changed and what information about it changed, so you generally don't need more information than that. – loganfsmyth Commented Mar 13, 2018 at 18:50
  • 1 Aside from that, if you're deleting a node, you typically don't care what happens to what's inside it. The case of "remove grandchild, then remove child" shouldn't even happen...but if it does, and you're cloning changes, then the "remove grandchild" doesn't affect the resulting document, so it should be ignorable. – cHao Commented Mar 13, 2018 at 18:56
  • @loganfsmyth: That's what I originally assumed, but it can't work that way. When I receive the first mutation, I see that x was removed from a p node, but at that time the p node already has no parent. So it's not enough information. When this happens I don't know to look at mutations[1] because I don't know what order the DOM Changes occurred in. – EricP Commented Mar 13, 2018 at 18:59
 |  Show 4 more ments

4 Answers 4

Reset to default 5 +200

The mutations array is a full list of mutations that have happened for a particular target. That means, for an arbitrary element, the only way to know what the parent was at the time of that mutation you'd have to look through the later mutations to see when the parent was mutated, e.g.

var target = mutations[0].target
var parentRemoveMutation = mutations
 .slice(1)
 .find(mutation => mutation.removedNodes.indexOf(target) !== -1);
var parentNode = parentRemoveMutation 
  ? parentRemoveMutation.target // If the node was removed, that target is the parent
  : target.parentNode; // Otherwise the existing parent is still accurate.

As you can see though, this is hard-coded for the first mutation, and you'd likely have to perform it for each item in the list one at a time. This won't scale very well as since it has to do linear searches. You could also potentially run through the full mutation list to build up that metadata first.

All that said, it seems like the core of the issue here is that you really shouldn't care about the parent in an ideal world. If you are synchronizing two documents for instance, you could consider using a WeakMap to track elements, so for each possible element, have a mapping from the document being mutated to each element in the synced document. Then when mutations happen, you can just use the Map to look up the corresponding element in the original document, and reproduce the changes on the original document without needing to look at the parent at all.

In the ments, you say that your goal is to clone changes from one document to another. As loganfsmyth suggests, the best way to do that would be to keep a (Weak)Map mapping the original nodes to their clones, and update that map each time you clone a new node. That way, your mutation observer can process the mutations one at a time, in the order they appear in the mutation list, and perform the corresponding operation on the mirror nodes.

Despite what you claim, this should not be particularly plicated to implement. Since a single snippet often speaks more than a thousand words, here's a simple example that clones any changes from one div to another:

var observed = document.getElementById('observed');
var mirror = document.getElementById('mirror');

var observer = new MutationObserver( updateMirror );
observer.observe( observed, { childList: true } );

var mirrorMap = new WeakMap ();

function updateMirror ( mutations ) {
  console.log( 'observed', mutations.length, 'mutations:' );
  
  for ( var mutation of mutations ) {
    if ( mutation.type !== 'childList' || mutation.target !== observed ) continue;
    
    // handle removals
    for ( var node of mutation.removedNodes ) {
      console.log( 'deleted', node );
      mirror.removeChild( mirrorMap.get(node) );
      mirrorMap.delete(node);  // not strictly necessary, since we're using a WeakMap
    }
    
    // handle insertions
    var next = (mutation.nextSibling && mirrorMap.get( mutation.nextSibling ));
    for ( var node of mutation.addedNodes ) {
      console.log( 'added', node, 'before', next );
      var copy = node.cloneNode(true);
      mirror.insertBefore( copy, next );
      mirrorMap.set(node, copy);
    }    
  }
}

// create some test nodes
var nodes = {};
'fee fie foe fum'.split(' ').forEach( key => {
  nodes[key] = document.createElement('span');
  nodes[key].textContent = key;
} );

// make some insertions and deletions
observed.appendChild( nodes.fee );  // fee
observed.appendChild( nodes.fie );  // fee fie
observed.insertBefore( nodes.foe, nodes.fie );  // fee foe fie
observed.insertBefore( nodes.fum, nodes.fee );  // fum fee foe fie
observed.removeChild( nodes.fie );  // fum fee foe
observed.removeChild( nodes.fee );  // fum foe
#observed { background: #faa }
#mirror { background: #afa }
#observed span, #mirror span { margin-right: 0.3em }
<div id="observed">observed: </div>
<div id="mirror">mirror: </div>

At least for me, on Chrome 65, this works perfectly. The console indicates that, as expected, the mutation observer callback is called once, with a list of six mutations:

observed 6 mutations:
added <span>fee</span> before null
added <span>fie</span> before null
added <span>foe</span> before <span>fie</span>
added <span>fum</span> before <span>fee</span>
deleted <span>fie</span>
deleted <span>fee</span>

As a result of mirroring these mutations, both the original div and its mirror end up with the spans "fum" and "foe" in that order.

The better idea is to check the addedNodes and removedNodes array. They contain Nodelist of HTML elements with previousSibling and nextSibling property pointing to exact previous and next element right now after mutation.

Change

    var insertImg = mutations[0];

To

    var insertImg = mutations[0].addedNodes[0];

<div id="d">text<br><hr>text</div>

<script>
    // Called when DOM changes.
    function mutationCallback(mutations) {
        // assert(mutations.length === 3);
        var insertImg = mutations[0].addedNodes[0];
        console.log(insertImg);
        console.log(insertImg.previousSibling);
        console.log(insertImg.nextSibling);
    }
  
    // Setup
    var div = document.getElementById('d');
    var br = div.childNodes[1];
    var hr = div.childNodes[2];
    var observer = new MutationObserver(mutationCallback);
    observer.observe(div, {childList: true, subtree: true});

    // Trigger DOM Changes.
    var img = document.createElement('img');
    d.insertBefore(img, hr);
    d.removeChild(hr);
    d.removeChild(br); // mutationCallback() is first called after this line.
</script>

The DOM manipulations such inserting, removing or moving elements are synchronous.

So you will not see the result until all the synchronous operations that follow one another are performed.

So you need to perform mutations asynchronously. A simple example:

// Called when DOM changes.
function mutationCallback(mutations) {
    var insertImg = mutations[0];
    console.log('mutation callback', insertImg.previousSibling.parentNode.outerHTML);
    console.log('mutation callback', insertImg.nextSibling.parentNode.outerHTML);
}

// Setup
var div = document.getElementById('d');
var br = div.childNodes[1];
var hr = div.childNodes[2];
var img = document.createElement('img');

var observer = new MutationObserver(mutationCallback);
observer.observe(div, {childList: true, subtree: true});

// Trigger DOM Changes.
setTimeout(function() {
  console.log('1 mutation start')
  d.insertBefore(img, hr);
  setTimeout(function (){
    console.log('2 mutation start')
    div.removeChild(hr);
    setTimeout(function (){
      console.log('3 mutation start')
      div.removeChild(br);
    }, 0)
  }, 0)
}, 0)
<div id="d">text<br><hr>text</div>

Or a more plex example with promises and async / await:

(async function () {
  function mutation(el, mand, ...params) {
    return new Promise(function(resolve, reject) {
      el[mand](...params)
      console.log(mand)
      resolve()
    })
  }

  await mutation(div, 'insertBefore', img, hr)
  await mutation(div, 'removeChild', hr)
  await mutation(div, 'removeChild', br)
})()

[ https://jsfiddle/tt5mz8zt/ ]

发布评论

评论列表(0)

  1. 暂无评论