MutationObserver 的 DOM 上下文丢失了,因为它触发得太晚了
Posted
技术标签:
【中文标题】MutationObserver 的 DOM 上下文丢失了,因为它触发得太晚了【英文标题】:MutationObserver's DOM context is lost because it fires too late 【发布时间】:2018-08-22 03:06:01 【问题描述】:我的代码的简化版:
<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>
我正在使用 Mutation Observers 来捕获 DOM 更改,以便在另一个文档实例发生更改时更新一个文档实例。因为在 的前一个和下一个兄弟元素被删除之后才会调用突变观察函数,因此 mutationCallback 函数无法判断它是在哪里插入的。在 Chrome、FF 和 IE11 中重现。
另一种方法是遍历整个文档以查找更改,但这会抵消使用 Mutation Observers 的性能优势。
【问题讨论】:
为什么要以.parentNode
开头? mutations[0].target
给了p
和mutations[0].removedNodes[0]
给你x
,然后分开,因为它是一个单独的突变,mutations[1].target
给div
和mutations[1].removedNodes[0]
给你p
。这些信息足以重建发生的事情。
我正在将更改从一个文档克隆到另一个文档。第一个文档中的操作可以以任何顺序发生。所以我不能简单地将mutations[1].target 硬编码到我的解决方案中。
完全可以,但我是说您需要将mutations
数组中的每个项目完全重播到您的另一个文档中,每个mutations
项目都会告诉您哪个节点发生了变化以及哪些信息关于它发生了变化,因此您通常不需要更多信息。
除此之外,如果您要删除一个节点,您通常不会关心其中的内容会发生什么。 “删除孙子,然后删除子”的情况甚至不应该发生......但如果确实发生了,并且您正在克隆更改,那么“删除孙子”不会影响生成的文档,所以它应该是可忽略的.
@loganfsmyth:这是我最初的假设,但它不能那样工作。当我收到第一个突变时,我看到 x 已从 p 节点中删除,但当时 p 节点已经没有父节点。所以信息不够。发生这种情况时,我不知道查看突变[1],因为我不知道 DOM 更改发生的顺序。
【参考方案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
来跟踪元素,因此对于每个可能的元素,都有一个从正在变异的文档到同步文档中的每个元素的映射。那么当发生突变时,您可以使用 Map 在原始文档中查找相应的元素,并在原始文档上重现更改,而无需查看父级。
【讨论】:
WeakMap 解决方案会很麻烦,因为我必须在每次插入操作后不断更新引用。但如果没有发布更好的解决方案,我会将其标记为已接受的解决方案。 我无法让这个 Weakmap 同步解决方案正常工作。考虑一下我使用 insertBefore 将 添加到 document.body,然后在 中添加“文本”的情况。当我使用第一个突变时,我看到 text 存在于原始文档中,并将其复制到镜像文档中。但是第二个突变再次添加了“文本”。但是,假设我只在第一步添加 而没有子级。在这种情况下,如果在将 添加到正文之前将“文本”添加为 的子项,它将失败。总之,我没有办法知道是否要克隆节点的子节点。 不是 100% 我按照您的示例进行操作,但一个建议是您可以查看 github.com/rafaelw/mutation-summary 之类的东西,它是实用程序 github.com/rafaelw/mutation-summary/blob/master/util/…。我自己没有在那里使用镜像,但我使用了 MutationSummary,它似乎工作得很好。【参考方案2】:更好的办法是检查addedNodes
和removedNodes
数组。它们包含 html 元素的 Nodelist,其中 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>
【讨论】:
这个想法很好,但是我想到了一个会失败的情况:假设我用insertBefore 3次插入节点B,然后节点A在节点B之前,然后节点C在节点B之后. 在这种情况下,当我的目标文档被要求插入 B 时,它会感到困惑,因为其中尚不存在 A 和 C。 您可以将此类元素移到数组的末尾以便在最后进行处理,可能使用队列。【参考方案3】:在 cmets 中,您说您的目标是将更改从一个文档克隆到另一个文档。 As loganfsmyth suggests, 最好的方法是保留将原始节点映射到其克隆的(弱)映射,并在每次克隆新节点时更新该映射。这样,您的突变观察者可以一次处理一个突变,按照它们在突变列表中出现的顺序,并在镜像节点上执行相应的操作。
尽管您声称,这应该不是特别复杂的实施。由于单个 sn-p 经常说一千多个单词,这里有一个简单的示例,可以将任何更改从一个 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>
至少对我来说,在 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 和它的镜像最终都以该顺序的跨度“fum”和“foe”结束。
【讨论】:
【参考方案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>
或者一个更复杂的例子,带有 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/]
【讨论】:
以上是关于MutationObserver 的 DOM 上下文丢失了,因为它触发得太晚了的主要内容,如果未能解决你的问题,请参考以下文章
MutationObserver 在整个 DOM 中检测节点的性能