MutationObserver DOM-контекст потерян, потому что он срабатывает слишком поздно

Упрощенная версия моего кода:

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

Ответ 1

Массив mutations - это полный список мутаций, которые произошли для конкретной цели. Это означает, что для произвольного элемента единственный способ узнать, что родитель был во время этой мутации, вам нужно было бы просмотреть более поздние мутации, чтобы увидеть, когда родитель был мутирован, например

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.

Как вы можете видеть, это жестко закодировано для первой мутации, и вам, вероятно, придется выполнять ее для каждого элемента в списке по одному за раз. Это не будет очень хорошо масштабироваться, так как оно должно выполнять линейные поиски. Вы также можете запустить полный список мутаций, чтобы сначала создать метаданные.

Все, что было сказано, похоже, что суть проблемы здесь в том, что вы действительно не должны заботиться о родителе в идеальном мире. Если вы синхронизируете, например, два документа, вы можете подумать о WeakMap чтобы использовать WeakMap для отслеживания элементов, поэтому для каждого возможного элемента есть сопоставление с документом, который мутируется каждому элементу в синхронизированном документе. Затем, когда происходят мутации, вы можете просто использовать карту, чтобы найти соответствующий элемент в исходном документе, и воспроизвести изменения в оригинальном документе, не глядя вообще на родителя.

Ответ 2

В комментариях вы говорите, что ваша цель - клонировать изменения из одного документа в другой. Как предполагает loganfsmyth, лучший способ сделать это - сохранить (слабую) карту, сопоставляя исходные узлы с их клонами, и обновлять эту карту каждый раз, когда вы клонируете новый узел. Таким образом, ваш мутационный наблюдатель может обрабатывать мутации по одному, в порядке их появления в списке мутаций и выполнять соответствующую операцию на зеркальных узлах.

Несмотря на то, что вы утверждаете, это не должно быть особенно сложно реализовать. Поскольку один фрагмент часто говорит более тысячи слов, вот простой пример, который клонирует любые изменения от одного div к другому:

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>

Ответ 3

Лучшая идея - проверить массив addedNodes и removedNodes. Они содержат Nodelist из HTML-элементов с свойством previousSibling и nextSibling указывающим на точный предыдущий и следующий элемент прямо сейчас после мутации.

+ Изменить

    var insertImg = mutations[0];

к

    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>

Ответ 4

Операции DOM, такие как вставка, удаление или перемещение элементов, являются синхронными.

Таким образом, вы не увидите результат до тех пор, пока не будут выполнены все синхронные операции, которые следуют друг за другом.

Таким образом, вам необходимо выполнить асинхронные мутации. Простой пример:

// 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>