首页 > 解决方案 > 快速调用点击事件时随机出现类型错误

问题描述

我正在创建一个小的 javascript 小程序,当新按钮之前没有被单击时,它只会生成一个新按钮。但是,当您快速单击按钮时,我随机得到“未捕获的 TypeError:无法读取未定义的属性 'innerHTML'”。为什么会出现这种情况?

代码:“ https://codepen.io/fyun89/pen/PgWMNg?editors=1111

let btn = document.getElementsByClassName('btn');
let container = document.getElementsByClassName('container');

const action = function(e) {
  // set up new button
  let newBtn = document.createElement('button');
  newBtn.innerHTML = 'this is has been clicked <span>0</span> times';

  // for new button decoration
  let color1 = Math.min(Math.floor((Math.random() * 1000)/3), 255);
  let color2 = Math.min(Math.floor((Math.random() * 1000)/3), 255);
  let color3 = Math.min(Math.floor((Math.random() * 1000)/3), 255);

  // if the button hasn't been clicked
  if (e.target.children[0].innerHTML < 1) {
    container[0].appendChild(newBtn);

    // target the last created child - button
    let currentElem = container[0].children;
    currentElem[currentElem.length - 1].setAttribute('class', 'btn');
    currentElem[currentElem.length - 1].style.backgroundColor = `rgb(${color1}, ${color2}, ${color3})`;
    currentElem[currentElem.length - 1].addEventListener('click', action);
  }

  // get the current element's count
  let numb = Number(e.target.children[0].innerHTML);
  e.target.children[0].innerHTML = numb + 1;
}

for (let i = 0; i < btn.length; i++) {
  btn[i].addEventListener('click', action);
}

控制台中有错误的图像

标签: javascripthtml

解决方案


您可以通过单击数字/跨度标签来重现该问题。

span 标签没有子标签,因此会引发错误。

你可以在这里阅读更多关于绑定事件如何从我们心爱的 CodePen 创造者那里传播到它的孩子的信息: https ://css-tricks.com/slightly-careful-sub-elements-clickable-things/

TLDR ; 要么检查你正在使用的元素是预期的元素,要么添加一些 CSS 以防止向下传播(CSS 控制这个恕我直言......)

✨⭐ 或者只是在您的代码中替换e.targete.currentTarget,以便使用该绑定元素,而不是包含其中包含的任何内容。

可能的修复

CSS 方法

如果按钮内部也不需要点击处理,则可以执行此操作。

.btn > * {
  pointer-events: none;
}

JS方法

CSS 技巧文章中的评论者提到使用currentTarget.

我试过了,它似乎工作

只需在您的代码中替换e.target为。e.currentTarget

另一个 JS 方法

const tagName = e.target.tagName.toLowerCase();
if (tagName === 'button') {
  // we have the button
}
else if (tagName === 'SPAN') {
  // we have the span
}
else {
  console.error('Unrecognized element', tagName, e.target)
}

错误的调试

发生错误时,请注意目标不是整个按钮,它只是跨度,并且跨度没有任何子级。

当您单击按钮中的数字(跨度)时会发生这种情况

调试错误

此外

从您的代码中有一个地方,事件处理程序不会在很短的时间内绑定,但可能存在于 DOM 中。我想由于渲染,您甚至可能无法单击以找到此错误,特别是因为它仅存在于新按钮上,但它以编程方式存在于代码中的时间很短。

您应该在使用之前绑定 click 事件appendChild以将其添加到 DOM。在您设置样式并添加事件处理之前,我会避免将您的元素添加到 DOM。

红色区域表示元素存在于 DOM 中但没有点击处理程序的时间。

事件在短时间内不存在的部分

同行评审和重构

清理第一轮——也许这个重构会有所帮助?

如果变量只分配一次并且不希望重新分配,我建议您使用const而不是。let尽管这听起来可能毫无用处,但它可以帮助阐明您定义的变量的意图。

我见过很多时候人们有范围和代码问题,只是简单地通过和设置明确的letvsconst有助于阐明这个问题。

在下面的重构中,我已更改letconst适用的地方,并对代码进行了 DRY,并使用描述性变量名称使某些部分更具可读性。

const btn = document.getElementsByClassName('btn');
const container = document.getElementsByClassName('container');

// name it for readability, but then run it
// let hosting bring action up
(function bindCurrentButtons() {
    for (let i = 0; i < btn.length; i++) {
    btn[i].addEventListener('click', action);
  }
})();

function action(e) {
  // set up new button
  const newBtn = document.createElement('button');
  newBtn.innerHTML = 'this is has been clicked <span>0</span> times';

  // for new button decoration
  const randomColor = () => Math.min(Math.floor((Math.random() * 1000) / 3), 255);
  const colorRed = randomColor();
  const colorGreen = randomColor();
  const colorBlue = randomColor();

  // if the button hasn't been clicked
  if (e.target.children[0].innerHTML < 1) {
    // target the last created child - button
    const currentElemChildren = container[0].children;
    const elementIndex = currentElemChildren.length - 1;
    const currentElem = currentElemChildren[elementIndex];

    currentElem.setAttribute('class', 'btn');
    currentElem.style.backgroundColor = `rgb(${colorRed}, ${colorGreen}, ${colorBlue})`;
    currentElem.addEventListener('click', action);

    container[0].appendChild(newBtn);
  }

  // get the current element's count
  const numb = Number(e.target.children[0].innerHTML);
  e.target.children[0].innerHTML = numb + 1;
}

清理第 2 轮 - 继续重构

这是一个更新的 CodePen。我确实删除了这些类以进一步简化它。

https://codepen.io/fyun89/pen/PgWMNg?editors=1111

const buttons = document.querySelectorAll('button');
const container = document.getElementById('container');

// name it for readability, but then run it right away
// let hosting bring other functions up
(function bindCurrentButtons() {
    for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', action);
  }
})();

function buttonCount(buttonElement) {
  const span = buttonElement.querySelector('span');
  return Number(span.innerText);
}

function incrementCounter(buttonElement) {
  const span = buttonElement.querySelector('span');
  const number = buttonCount(buttonElement) + 1;
  span.innerText = number.toString();
}

const randomColor = () => Math.min(Math.floor((Math.random() * 1000) / 3), 255);
const randomRgbColor = () => `rgb(${randomColor()}, ${randomColor()}, ${randomColor()})`;

function action(e) {
  const button = e.currentTarget;
  const currentCount = buttonCount(button);

  incrementCounter(button);

  if (currentCount === 0) {
    const newButton = createNewButton();
    container.appendChild(newButton);
  }
}

function createNewButton() {
  // set up new button
  const newButton = document.createElement('button');
  newButton.innerHTML = 'this is has been clicked <span>0</span> times';
  newButton.style.backgroundColor = randomRgbColor();
  newButton.addEventListener('click', action);
  return newButton;
}

第 3 轮重构 - JS 类

我已将 HTML 更改为不以按钮开头。现在在 Javascript 中创建了初始按钮。它现在以随机颜色按钮而不是橙色按钮开始。该类现在管理状态/计数器。我还删除了跨度,因为它不再需要用作数据存储。

https://codepen.io/codyswartz/pen/GLmbvW?editors=0011

  const randomColor = () => Math.min(Math.floor((Math.random() * 1000) / 3), 255);
  const randomRgbColor = () => `rgb(${randomColor()}, ${randomColor()}, ${randomColor()})`;

  class CounterButton {

    constructor(containerElement) {
      this.counter = 0;
      this.container = containerElement;
      this.button = this.createElement();

      this.updateCounter();
      this.randomBackgroundColor();
      this.bindClickEvent();
      this.addToDom();
    }

    createElement() {
      return document.createElement('button');
    }

    updateCounter() {
      this.button.innerHTML = `this is has been clicked ${this.counter} times`;
    }

    randomBackgroundColor() {
      this.button.style.backgroundColor = randomRgbColor();
    }

    bindClickEvent() {
      this.button.addEventListener('click', this.clickEvent.bind(this));
    }

    clickEvent() {
      if(this.counter === 0) {
        new CounterButton(this.container);
      }

      this.counter++;
      this.updateCounter();
    }

    addToDom() {
      this.container.appendChild(this.button);
    }

  }

  const container = document.getElementById('container');
  new CounterButton(container);


推荐阅读