从 disconnectedCallback 中的 DOM 中删除自定义元素的所有引用为空

Null all references of custom element being removed from the DOM in disconnectedCallback

我有一个自定义元素提供 API 方法 sayHello。如果元素从 DOM 中删除,我需要 "destroy" disconnectedCallback 中对自定义元素的所有引用。我怎样才能做到这一点?

class RemoveEl extends HTMLButtonElement {
  constructor() {
    super();
    this.type= 'button';
    this.addEventListener('click', () => {
      this.parentElement.removeChild(this);
    })
  }
  
  sayHello() {
    console.log('hello');
  }
  
  disconnectedCallback() {
    if (!document.body.contains(this)) {
      console.log('removed');
      // here I need something like 
      // this = null;
    }
  }
}

customElements.define('remove-el', RemoveEl, { extends: 'button' });

var sayHello = document.getElementById('sayHello');
var removeEl = document.getElementById('removeEl');

sayHello.addEventListener('click', () => {
  if (removeEl) {
    removeEl.sayHello();
  }
})
<div>Test:
  <button is="remove-el" id="removeEl">Click to remove</button>
  <button id="sayHello" type="button">Say Hello</button>
</div>
  

根据我的理解,只要您在 JavaScript 端持有对元素的引用,垃圾收集器就无法销毁您的元素,即使它已从 DOM。您的元素仍然存在,您将能够调用它的方法。

您必须自己管理参考资料。在自定义元素的 disconnectedCallback 中,设置一个 属性 以将其标记为已删除,例如:this.destroyed = true.

然后您可以使用那个 属性 来保护访问,但该元素不会被垃圾收集:

class RemoveEl extends HTMLButtonElement {
  constructor() {
    super();
    this.type= 'button';
    this.addEventListener('click', () => {
      this.parentElement.removeChild(this);
    })
  }
  
  sayHello() {
    console.log('hello');
  }
  
  disconnectedCallback() {
    if (!document.body.contains(this)) {
      this.destroyed = true;
      console.log('removed');
    }
  }
}

customElements.define('remove-el', RemoveEl, { extends: 'button' });

const sayHello = document.getElementById('sayHello');
const removeEl = document.getElementById('removeEl');

sayHello.addEventListener('click', () => {
  if (removeEl && !removeEl.destroyed) {
    removeEl.sayHello();
  }
})
<div>Test:
  <button is="remove-el" id="removeEl">Click to remove</button>
  <button id="sayHello" type="button">Say Hello</button>
</div>

或者创建一个引用包装器,仅当内部引用有效时才能在其上应用函数,垃圾收集仍然无法销毁该引用,因为现在由于 do 函数使用 el:

class RemoveEl extends HTMLButtonElement {
  constructor() {
    super();
    this.type= 'button';
    this.addEventListener('click', () => {
      this.parentElement.removeChild(this);
    })
  }
  
  sayHello() {
    console.log('hello');
  }
  
  disconnectedCallback() {
    if (!document.body.contains(this)) {
      this.destroyed = true;
      console.log('removed');
    }
  }
}

customElements.define('remove-el', RemoveEl, { extends: 'button' });

const ref = el => ({ do: fn => { if (el && !el.destroyed) fn(el); }  })

const sayHello = document.getElementById('sayHello');
const removeEl = ref(document.getElementById('removeEl'));

sayHello.addEventListener('click', () => {
 removeEl.do(el => el.sayHello());
})
<div>Test:
  <button is="remove-el" id="removeEl">Click to remove</button>
  <button id="sayHello" type="button">Say Hello</button>
</div>

或者您可以使用代理来管理该引用。只要 destroyed 为假,就会在对象上调用这些方法,但是一旦代理检测到 destroyed = true,它就会 return 属性的默认值并销毁它自己对该元素的引用,这有望让垃圾收集器摆脱它。

有点像这样:

class RemoveEl extends HTMLButtonElement {
  constructor() {
    super();
    this.type= 'button';
    this.addEventListener('click', () => {
      this.parentElement.removeChild(this);
    })
  }
  
  sayHello() {
    console.log('hello');
  }
  
  disconnectedCallback() {
    if (!document.body.contains(this)) {
      this.destroyed = true;
      console.log('removed');
    }
  }
}

customElements.define('remove-el', RemoveEl, { extends: 'button' });

const ref = (el, defaultEl) => {
  let destroyed = el.destroyed;
  const checkEl = () => {
    if (!destroyed && el && el.destroyed) {
      destroyed = true;
      el = null;
    }
    return destroyed;
  }
  return new Proxy({}, {
    get: (obj, prop) => {
      return checkEl() ? defaultEl[prop] : el[prop];
    }
  });
}

const sayHello = document.getElementById('sayHello');
const removeEl = ref(document.getElementById('removeEl'), { sayHello: () => console.log('bye') });

sayHello.addEventListener('click', () => {
  removeEl.sayHello();
})
<div>Test:
  <button is="remove-el" id="removeEl">Click to remove</button>
  <button id="sayHello" type="button">Say Hello</button>
</div>

有 2 个解决方案:

不要保留任何引用

自定义元素从 DOM 中删除后立即被垃圾收集的方式。

//var removeEl = document.getElementById('removeEl');

sayHello.addEventListener('click', () => {
    let removeEl = document.getElementById('removeEl')
    if ( removeEl )
        removeEl.sayHello();   
})

管理全局引用

如果您需要保留对自定义元素的全局引用,则需要将其设置为 null 以销毁该对象。

您可以通过多种方式实现这一目标。例如,当元素断开连接时调度自定义事件并在引用级别处理它。

class RemoveEl extends HTMLButtonElement {
  constructor() {
    super();
    this.addEventListener('click', () => this.parentElement.removeChild(this));
  }
  
  sayHello() {
    console.log('hello');
  }
  
  disconnectedCallback() {
    console.log('removed');
    //dispatch a destroy event
    var ev = new CustomEvent('destroyed');
    document.dispatchEvent(ev);
  }
}

customElements.define('remove-el', RemoveEl, { extends: 'button' });

var sayHello = document.getElementById('sayHello');
var removeEl = document.getElementById('removeEl');
//delete reference
document.addEventListener('destroyed', () => removeEl = null);

sayHello.addEventListener('click', () => removeEl && removeEl.sayHello())
<button is="remove-el" id="removeEl">Click to remove</button>
<button id="sayHello">Say Hello</button>