在不牺牲性能的情况下检测给定元素已从 DOM 中删除

Detect that given element has been removed from the DOM without sacrificing performance

我有这些:

const element = this.getElementById("victim")

function releaseKraken(targetElement) {}

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

我可以想象这样的事情:

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

我知道我需要 MutationObserver,但我找到的所有文档都集中在观察给定元素的 children,而我需要观察元素本身。

UPD:问题 如何从 dom 元素中检测元素是 added/removed? 专注于观看给定 parent 的 children。我不想看 children 或 parent。当从 DOM 中删除给定元素时,我想收到通知。不是 children。而且我不想在给定元素的 parent 上设置观察者(除非这是唯一的选择),因为它会影响性能。

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

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

这是我在 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>

编辑

我稍微编辑了片段。您有两个选择:

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

正如您所说,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 优秀解决方案的另一种较短版本:

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!");
})

要观察特定元素是否已从 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();