attributeChangedCallback() 总是调用两次以多个事件侦听器结束

attributeChangedCallback() always called twice ending up with multiple event listeners

尝试使用自定义元素,我试图根据自定义元素属性的值触发点击事件。但是使用 attributeChangedCallback() 方法(以及 connectedCallback() 方法)我最终得到了多个事件监听器。

class HelloWorld extends HTMLElement {
  static get observedAttributes() {
    return ['attribute1', 'attribute2'];
  }

  get attribute1() {
    return this.getAttribute('attribute1');
  }
  set attribute1(val) {
    if (val) {
      this.setAttribute('attribute1', val);
    } else {
      this.removeAttribute('attribute1');
    }
  }

  get attribute2() {
    return this.getAttribute('attribute2');
  }
  set attribute2(val) {
    if (val) {
      this.setAttribute('attribute2', val);
    } else {
      this.removeAttribute('attribute2');
    }
  }

  constructor() {
    super();
  }

  connectedCallback() {
    this.textContent = 'Hello World';
    update(this);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    update(this);
  }
}
customElements.define('hello-world', HelloWorld);

function update(el) {
  if (el.attribute1 === 'foo') {
    el.addEventListener('click', e => {
      el.textContent = (el.textContent.indexOf(' (') != -1 ? el.textContent.substring(0, el.textContent.indexOf(' (')) : el.textContent) + ' (clicked ' + (el.textContent.match(/\d+/) ? parseInt(el.textContent.match(/-?\d+/)[0]) + 1 : 1) + ' times)';
    });
  } else if (el.attribute1 === 'bar') {
    el.addEventListener('click', e => {
      el.textContent = (el.textContent.indexOf(' (') != -1 ? el.textContent.substring(0, el.textContent.indexOf(' (')) : el.textContent) + ' (clicked ' + (el.textContent.match(/\d+/) ? parseInt(el.textContent.match(/-?\d+/)[0]) - 1 : 1) + ' times)';
    });
  }
}
hello-world {
  cursor: pointer;
}
<hello-world attribute1="foo" attribute2=""></hello-world>

为什么 attributeChangedCallback() 方法总是被调用两次,因此添加了两个事件侦听器?如何避免这种情况?最佳做法是什么?

它被称为 attributeChangedCallback,因此在每次 单个 属性更改时触发,包括元素添加到 DOM[=22= 时的初始化]

connectedCallback 中附加侦听器,但是如果您在 DOM 中移动元素,那么 可以 运行 再次在 disconnectedCallback

中删除它们

使用 内联 事件处理程序可能更简单,一个元素上只能有一个。

customElements.define('hello-world', class extends HTMLElement {
  static get observedAttributes() {
    return ['attribute1', 'attribute2'];
  }
  get attribute1() {
    return this.getAttribute('attribute1');
  }
  set attribute1(val) {
    this.toggleAttribute('attribute1', val);
  }
  get attribute2() {
    return this.getAttribute('attribute2');
  }
  set attribute2(val) {
    this.toggleAttribute('attribute2', val);
  }
  connectedCallback() {
    this.count = 0;
    this.innerHTML = `Hello World clicked: <span>${this.count}</span> times`;
    this.onclick = (evt) => {
      this.count++;
      this.querySelector("span").innerHTML = this.count;
    }
  }
  attributeChangedCallback(name, oldValue, newValue) {
    console.log("attributeChangedCallback", name, oldValue, newValue);
  }
});
hello-world {
  cursor: pointer;
}
<hello-world attribute1="foo" attribute2=""></hello-world>

constructor() {
  super()
}

不是必需的,不存在的 constructor 将从其父级执行 constructor.. 这就是 super() 所做的。

如果你想防止多个监听器,你可以尝试一个方法:

addListeners(){
  .. add your listeners

  this.addListeners = () => {}; // overload; Don't run its original code again
}

另请注意,您在元素(或其内容)上添加的侦听器会自动 垃圾收集/删除 当元素从DOM.
您在其他 DOM 元素(例如文档)上添加的任何监听器都必须在 disconnectedCallback

中自行删除