MutationObserver的DOM上下文丢失,因为它太晚了

时间:2018-03-13 18:22:57

标签: javascript dom mutation-observers

我的代码的简化版本:



<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>
&#13;
&#13;
&#13;

我正在使用Mutation Observers捕获DOM更改,以便在另一个文档实例发生更改时更新。因为直到&lt; img&gt;的前一个和下一个兄弟被删除之后才调用变异观察器函数,所以mutationCallback函数不能告诉它插入的位置。在Chrome,FF和IE11中重现。

另一种方法是遍历整个文档以查找更改,但这会否定使用Mutation Observers的性能优势。

4 个答案:

答案 0 :(得分:4)

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来跟踪元素,因此对于每个可能的元素,都要有一个映射,从正在变异的文档到同步文档中的每个元素。然后,当发生突变时,您可以使用Map查找原始文档中的相应元素,并在原始文档上重现更改,而无需查看父项。

答案 1 :(得分:2)

在评论中,您说您的目标是将更改从一个文档克隆到另一个文档。 As loganfsmyth suggests,最好的方法是保持(弱)映射将原始节点映射到其克隆,并在每次克隆新节点时更新该映射。这样,你的变异观察者可以按照它们出现在变异列表中的顺序一次一个地处理突变,并在镜像节点上执行相应的操作。

尽管您声称,但实施起来并不是特别复杂。由于单个片段通常会说超过一千个单词,因此这里有一个简单的例子可以克隆从一个div到另一个div的任何变化:

&#13;
&#13;
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
&#13;
#observed { background: #faa }
#mirror { background: #afa }
#observed span, #mirror span { margin-right: 0.3em }
&#13;
<div id="observed">observed: </div>
<div id="mirror">mirror: </div>
&#13;
&#13;
&#13;

至少对我而言,在Chrome 65上,这非常有效。控制台指出,正如预期的那样,突变观察者回调被调用一次,其中列出了六个突变:

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>

由于镜像了这些突变,原来的div和它的镜子最终都会出现跨越&#34; fum&#34;和&#34;敌人&#34;按此顺序。

答案 2 :(得分:1)

更好的想法是检查addedNodesremovedNodes数组。它们包含HTML元素的Nodelist,previousSiblingnextSibling属性指向突变后的精确上一个和下一个元素。

更改

    var insertImg = mutations[0];

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

&#13;
&#13;
<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>
&#13;
&#13;
&#13;

答案 3 :(得分:0)

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>

使用promises和async / await的更复杂的例子:

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

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

[https://jsfiddle.net/tt5mz8zt/]