JS_*_*ler 5 javascript dom mutation-observers
我想在创建特定 DOMNode 的上下文中检测何时将特定 DOMNode 添加到文档中。
这是我到目前为止所拥有的:
function getThingThatFormatsItself() {
const MY_NODE = createNode();
const observer = new MutationObserver(function (records) {
records.forEach(record => {
record.addedNodes.forEach(n => {
if (n === MY_NODE) {
observer.disconnect();
doFormatting();
}
});
})
});
observer.observe(document, {childList: true, subtree: true});
// do formatting stuff that relies on the element being in DOM
function doFormatting() {
console.log(`It's been added.`);
}
return MY_NODE;
}
/* ELSEWHERE IN MY CODE */
// Now that Thing is added to DOM, it can format itself.
// However, this doesn't work unless it's added via setTimeout!
// Also, in the meantime, we are needlessly iterating through every single node added to the document.
$("#Foo").append(getThingThatFormatsItself());
Run Code Online (Sandbox Code Playgroud)
这有两个问题:
setTimeout除非在将 thingNode 添加到文档之前有一个,否则这不起作用。看来.observe()不会立即生效。这是真的?有没有办法查看我的节点何时添加,而不必依赖使用 setTimeout 的外部调用者,并且不必同时遍历每个添加的节点?
说得好听一点,我无法观察实际节点本身的添加和删除,这真的很“令人困惑”——只能观察它的子节点。一些设计。这也相当“令人困惑”,.observe()似乎是放在事件队列中而不是立即执行。
MutationObserver 回调在微任务队列处理阶段的事件循环结束时运行,该阶段发生在主代码阶段完成之后,这就是为什么在当前运行的代码完成后调用 doFormatting() (可以说是整个函数调用堆栈)。
除非您的其他代码中有其他内容假设在当前事件循环中调用 doFormatting 或依赖于更新的 DOM 布局,否则它应该与使用 setTimeout 大致相同,后者安排回调在下一个事件循环周期中运行。
MutationObserver 累积批量突变并将其全部报告到微任务队列中的原因是,与已弃用的同步 DOM 突变事件相比,提供更快的观察能力。
解决方案 1:在 doFormatting() 之后使用回调运行代码
function onNodeAdopted(node, callback) {
new MutationObserver((mutations, observer) => {
if (node.parentNode) {
observer.disconnect();
callback(node);
}
}).observe(document, {childList: true, subtree: true});
return node;
}
function getThingThatFormatsItself(callback) {
return onNodeAdopted(createNode(), node => {
doFormatting(node);
console.log('Formatted');
callback(node);
});
}
$("#Foo").append(getThingThatFormatsItself(node => {
console.log('This runs after doFormatting()');
doMoreThings();
}));
console.log('This runs BEFORE doFormatting() as MutationObserver is asynchronous')
Run Code Online (Sandbox Code Playgroud)
解决方案2:不要使用 MutationObserver,而是拦截 Node.prototype.appendChild :
const formatOnAppend = (() => {
const hooks = new Map();
let appendChild;
function appendChildHook(node) {
appendChild.call(this, node);
const fn = hooks.get(node);
if (fn) {
hooks.delete(node);
// only restore if no one chained later
if (!hooks.size && Node.prototype.appendChild === appendChildHook) {
Node.prototype.appendChild = appendChild;
}
fn(node);
}
return node;
}
return {
register(node, callback) {
if (!hooks.size) {
appendChild = Node.prototype.appendChild;
Node.prototype.appendChild = appendChildHook;
}
hooks.set(node, callback);
return node;
},
}
})();
Run Code Online (Sandbox Code Playgroud)
用法:
function getThingThatFormatsItself() {
return formatOnAppend.register(createNode(), node => {
console.log('%o is added', node);
});
}
Run Code Online (Sandbox Code Playgroud)
其他可以尝试的事情:window.queueMicrotask(callback)而不是 setTimeout 将一些依赖代码排入微任务队列中。对于较旧的浏览器,本文中有一个简单的填充代码。
检查document.contains(MY_NODE)(如果在 ShadowDOM 内部则无济于事)或MY_NODE.parentNode代替枚举突变:
new MutationObserver((mutations, observer) => {
if (MY_NODE.parentNode) {
observer.disconnect();
doFormatting();
}
}).observe(document, {childList: true, subtree: true});
Run Code Online (Sandbox Code Playgroud)
这也更可靠,因为在一般情况下,该节点可能是另一个添加的节点的子节点,而不是作为 linkedNodes 数组中的单独项。