MutationObserver 的 DOM 上下文丢失,因为它触发得太晚
MutationObserver's DOM context is lost because it fires too late
我的代码的简化版本:
<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 的性能优势。
mutations
数组是针对特定目标发生的突变的完整列表。这意味着,对于任意元素,了解 parent 在该突变时 是什么的唯一方法 你必须查看后来的突变以了解何时parent 发生了变异,例如
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.
如您所见,这是第一个突变 hard-coded,您可能必须一次对列表中的每个项目执行一次。这不会很好地扩展,因为它必须进行线性搜索。您还可以 运行 通过完整的突变列表来首先构建该元数据。
综上所述,问题的核心似乎是您真的不应该关心理想世界中的 parent。例如,如果您正在同步两个文档,您可以考虑使用 WeakMap
来跟踪元素,因此对于每个可能的元素,都有一个从正在变异的文档到同步文档中的每个元素的映射。那么当发生突变时,你可以直接使用Map在原始文档中查找相应的元素,并在原始文档上重现更改,而根本不需要查看parent。
更好的办法是检查 addedNodes
和 removedNodes
数组。它们包含 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>
在评论中,您说您的目标是将更改从一个文档克隆到另一个文档。 最好的方法是保留一个(弱)映射将原始节点映射到它们的克隆节点,并在每次克隆新节点时更新该映射。这样,你的变异观察者就可以一次处理一个变异,按照它们在变异列表中出现的顺序,并在镜像节点上执行相应的操作。
不管您怎么说,实施起来应该不会特别复杂。由于单个片段通常会说出一千多个单词,这里有一个简单的示例,可以将任何更改从一个 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" 的顺序结束。
插入、删除或移动元素等 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)
})()
我的代码的简化版本:
<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 的性能优势。
mutations
数组是针对特定目标发生的突变的完整列表。这意味着,对于任意元素,了解 parent 在该突变时 是什么的唯一方法 你必须查看后来的突变以了解何时parent 发生了变异,例如
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.
如您所见,这是第一个突变 hard-coded,您可能必须一次对列表中的每个项目执行一次。这不会很好地扩展,因为它必须进行线性搜索。您还可以 运行 通过完整的突变列表来首先构建该元数据。
综上所述,问题的核心似乎是您真的不应该关心理想世界中的 parent。例如,如果您正在同步两个文档,您可以考虑使用 WeakMap
来跟踪元素,因此对于每个可能的元素,都有一个从正在变异的文档到同步文档中的每个元素的映射。那么当发生突变时,你可以直接使用Map在原始文档中查找相应的元素,并在原始文档上重现更改,而根本不需要查看parent。
更好的办法是检查 addedNodes
和 removedNodes
数组。它们包含 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>
在评论中,您说您的目标是将更改从一个文档克隆到另一个文档。
不管您怎么说,实施起来应该不会特别复杂。由于单个片段通常会说出一千多个单词,这里有一个简单的示例,可以将任何更改从一个 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" 的顺序结束。
插入、删除或移动元素等 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)
})()