【问题标题】:Detect that given element has been removed from the DOM without sacrificing performance在不牺牲性能的情况下检测给定元素已从 DOM 中删除
【发布时间】:2018-05-17 12:24:07
【问题描述】:

我有这些:

const element = this.getElementById("victim")

function releaseKraken(targetElement) {}

我希望在从 DOM 中删除 element 时调用该函数。

我可以想象这样的事情:

element.onRemove(() => releaseKraken(element))

我知道我需要MutationObserver,但我发现的所有文档都集中在观察给定元素的子元素上,而我需要观察元素本身。

UPD:问题How to detect element being added/removed from dom element? 侧重于观察给定父母的孩子。我不想看孩子或父母。当给定元素从 DOM 中删除时,我希望得到通知。不是孩子。而且我不想在给定元素的父元素上设置观察者(除非这是唯一的选择),因为这会影响性能。

UPD2:如果我在document 上设置MutationObserver,这将导致每个会话触发数千甚至数百万次回调,并且每次回调都必须过滤大量已删除元素的列表看看它是否包含有问题的那个。这简直是​​疯了。

我需要像上面展示的那样简单的东西。我希望回调只触发一次:当给定元素被删除时。

【问题讨论】:

  • 唯一的其他方法是弃用的 DOMNodeRemoved 事件。它可能不会影响性能,因为您只会观察节点本身。
  • 请先做一些研究,然后提出问题。
  • @lolmaus-AndreyMikhaylov 你想要一个观察者,但你不希望它观察整个身体,但它也不能观察任何父母。什么给了??
  • 同意 - 它仍然很丑!是的,关于 MutationObserver 一次清除所有不观察的好点 - 再次丑陋。

标签: javascript html dom mutation-observers


【解决方案1】:

正如你所说,MutationObserver 只允许你检测元素的子元素何时被操纵。这意味着您需要倾听父元素并检查进行了哪些更改以查看目标元素是否已被删除。

function onRemove(element, callback) {
  const parent = element.parentNode;
  if (!parent) throw new Error("The node must already be attached");

  const obs = new MutationObserver(mutations => {
    for (const mutation of mutations) {
      for (const el of mutation.removedNodes) {
        if (el === element) {
          obs.disconnect();
          callback();
        }
      }
    }
  });
  obs.observe(parent, {
    childList: true,
  });
}

然后用你的例子代替

element.onRemove(() => releaseKraken(element));

你可以的

onRemove(element, () => releaseKraken(element));

如果您所做的只是观察单个元素,这种方法应该会非常快。虽然这看起来是相当数量的循环,但removedNodes 很少有多个节点,除非有什么东西同时删除大量的兄弟姐妹,否则mutations 也将非常小。

你也可以考虑做

callback(el);

这会让你这样做

onRemove(element, releaseKraken);

【讨论】:

  • 谢谢你,@loganfsmyth。这正是我目前使用的东西,看起来它是轻而易举的。
  • 请考虑对您的答案进行此编辑(以我的 SO 分数,如果不立即申请,我无法提出):gist.github.com/lolmaus/98d8c95fbd1e0a2fa59aa77a574163b9/…
  • 嘿,@loganfsmyth!我们遇到了这个解决方案的问题。问题是当一个节点被删除时,removedNodes 中只有那个节点和它的子节点都没有出现。由于可以删除任何给定元素的父元素,因此我们需要 $(givenElement).parents().toArray().includes(removedNode) 之类的东西,而不是 if (el === element)。此外,我们必须注意document 而不是直接父级,并将subtree: true 包含到observe() 选项中。
  • 是的,没错。您从未真正指定它是如何被删除的,所以我认为这将是该特定元素的正常删除。
【解决方案2】:

loganfsmyth 出色解决方案的替代、较短版本:

function onRemove(el, callback) {
  new MutationObserver((mutations, observer) => {
    if(!document.body.contains(el)) {
      observer.disconnect();
      callback();
    }
  }).observe(document.body, { childList: true });
}

用法同理:

onRemove(myElement, function() {
  console.log("The element was removed!");
})

【讨论】:

  • 如果el.parentNodeparent 被删除了怎么办?回调还会触发吗?
  • @DavidGiven 好点!固定的。虽然不确定性能影响。当然可以随意更改。
【解决方案3】:

这是我在this 页面上找到的解决方案

document.getElementById("delete_one_div").addEventListener('click', function() {
  var divToDelete = document.getElementsByTagName("div")[0];
  divToDelete.parentNode.removeChild(divToDelete);
});

var element = document.getElementById("div_to_be_watched")
var in_dom = document.body.contains(element);
var observer = new MutationObserver(function(mutations) {
  if (in_dom && !document.body.contains(element)) {
    console.log("I was just removed");
    in_dom = false;
    observer.disconnect();
  }

});
observer.observe(document.body, { childList: true });
<div id="test">Test</div>
<div id="div_to_be_watched">Div to be watched</div>
<div class="div_to_be_watched">Second test</div>
<button id="delete_one_div">Delete one div</button>

编辑

我稍微编辑了 sn-p。你有两个选择:

  1. 按原样使用它。而且它不是很消耗内存,因为if 条件并不复杂(仅检查主体是否包含元素),它只观察到删除的那一刻,然后停止,
  2. 让观察者只观察特定元素以限制事件触发。

【讨论】:

  • 您的回调将被触发数千或数百万次,即使在元素被移除后也会如此。对于一个简单、狭窄的任务来说,这简直是疯狂的开销。
【解决方案4】:

要观察特定元素是否从 DOM 中删除,您可以使用以下函数。 (如果您不想将其用作 ES6 模块,请删除 export 关键字。) 干杯! ?

// Usage example:
// observeRemoval(document.children[0], doSomething, 'document.children[0] removed') => doSomething('document.children[0] removed') will be executed if document.children[0] is removed
// observeRemoval(document.children[0], doSomething) => doSomething(document.children[0]) will be executed if document.children[0] is removed (removed element is passed to callback function)
// observeRemoval([document.children[0], document.children[1]], doSomething) => doSomething(document.children[0]) will be executed if document.children[0] is removed (removed element is passed to callback function), doSomething(document.children[1]) will be executed if document.children[1] is removed
// observeRemoval([document.children[0], document.children[1]], doSomething, ['document.children[0] removed', 'document.children[1] removed']) => doSomething('document.children[0] removed') will be executed if document.children[0] is removed, doSomething('document.children[1] removed') will be executed if document.children[1] is removed
// observeRemoval(document.querySelectorAll('body *'), doSomething) => doSomething(<removed-element>) will be executed if any element inside the document.body is removed

export function observeRemoval(elements, callback, callbackInputs){
  let ecr = ecrTransform(elements, callback, callbackInputs);
  for(let i=0;i<ecr.elements.length;i++){
    let match = removalObserved.find(obj => obj.element === ecr.elements[i] && obj.callback === ecr.callback && obj.callbackInput === ecr.callbackInputs[i]);
    if(!match){
      removalObserved.push({
        element: ecr.elements[i],
        callback: ecr.callback,
        callbackInput: ecr.callbackInputs[i],
      });
    }
  }
}

export function unobserveRemoval(elements, callback, callbackInputs){
  let ecr = ecrTransform(elements, callback, callbackInputs);
  for(let i=0;i<removalObserved.length;i++){
    let index = removalObserved.findIndex(obj => obj.element === ecr.elements[i] && obj.callback === ecr.callback && obj.callbackInput === ecr.callbackInputs[i]);
    if(index > -1){
      removalObserved.splice(index, 1);
    }
  }
}

export function getRemovalObservers(elements, callback, callbackInputs){
  return removalObserved;
}

export function disconnectRemovalObserver(){
  removalObserver.disconnect();
}

export function connectRemovalObserver(){
  removalObserver.observe(document, {childList: true, subtree: true});
}

function ecrTransform(elements, callback, callbackInputs){
  elements = transformNodeListHTMLCollectionToArray(elements);
  callbackInputs = transformNodeListHTMLCollectionToArray(callbackInputs);
  if(!Array.isArray(elements)){
    elements = [elements];
  }
  if(!Array.isArray(callbackInputs)){
    callbackInputs = [callbackInputs];
    if(callbackInputs[0] === undefined){
      callbackInputs[0] = elements[0];
    }
  }
  if(elements.length > callbackInputs.length){
    // let continuouscallbackInput = callbackInputs[callbackInputs.length-1];
    // for(let i=0;i<(elements.length - callbackInputs.length);i++){
    //   callbackInputs.push(continuouscallbackInput);
    // }
    let continuouscallbackInput = callbackInputs[callbackInputs.length-1];
    for(let i=(elements.length - callbackInputs.length);i<elements.length;i++){
      callbackInputs.push(elements[i]);
    }
  }
  return {elements, callback, callbackInputs};
}
function transformNodeListHTMLCollectionToArray(list){
  if(NodeList.prototype.isPrototypeOf(list) || HTMLCollection.prototype.isPrototypeOf(list)){
    return Array.from(list);
  }
  return list;
}

const removalObserved = [];
const removalObserver = new MutationObserver(mutations => {
  for(let m=0;m<mutations.length;m++){
    for(let i=0;i<mutations[m].removedNodes.length;i++){
      let dO = removalObserved;
        for(let j=0;j<dO.length;j++){
        if(mutations[m].removedNodes[i].contains(dO[j].element) && !document.contains(dO[j].element)){
          (dO[j].callbackInput !== undefined) ? dO[j].callback(dO[j].callbackInput) : dO[j].callback(dO[j].element);
        }
      }
    }
  }
});
connectRemovalObserver();

【讨论】:

  • 感谢您的意见,但这仍然需要观察父母并遍历他们所有的孩子,这并不完美。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-10-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-19
  • 1970-01-01
相关资源
最近更新 更多