快速调用点击事件时随机发生类型错误

时间:2019-04-10 05:48:22

标签: javascript html

我正在创建一个小的javascript小程序,当以前从未单击过新按钮时,它将仅产生一次新按钮。但是,当您快速单击按钮时,我随机得到“ Uncaught 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);
}

Image with error in console

3 个答案:

答案 0 :(得分:2)

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

span标记没有子代,因此引发错误。

您可以在这里从我们心爱的CodePen创建者那里了解有关绑定事件如何向其子对象滴流的更多信息: https://css-tricks.com/slightly-careful-sub-elements-clickable-things/

TLDR ;要么检查您正在使用的元素是期望的元素,要么添加一些CSS以防止向下传播(总的来说CSS正在控制此IMHO ...)

✨⭐或只需将代码中的e.target替换为e.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)
}

错误的调试

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

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

Debugging the error

此外

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

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

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

Section where the event does not exist for a small period of time

同行评审和重构

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

如果变量仅分配一次并且不希望重新分配,我建议您使用const而不是let。尽管这听起来可能没有用,但可以帮助您了解所定义变量的意图。

我见过很多时候人们遇到了范围和代码方面的问题,只需仔细研究并设置明确的letconst有助于阐明问题。

在下面的重构中,我在适用的地方将let更改为const,并且将代码干了一些,并用描述性变量名使某些部分更易读。

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;
}

第二轮清理-持续重构

这是更新的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);

答案 1 :(得分:1)

该错误不能始终如一地再现。对于短期而言,您可以使用以下代码来解决该错误。

 if (e.target.children.length > 0){
    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);
      }
  }

答案 2 :(得分:1)

您可能会发现使用去抖动器很有帮助,看看是否有帮助。您可能已经知道,但是去抖动器确保对于可能发生很多次的事件仅发送一个信号。(另请注意,还有一种称为“节流”的功能,它限制了函数在固定时间间隔内收到的调用-例如,每500ms仅发生一个事件。)或者,如Chris Coyer explains the two

限制功能可随时间推移强制调用函数的最大次数。就像“每100毫秒最多执行一次此功能”。

去抖强制执行一个函数,直到经过一定时间后才再次调用该函数。就像“只有在没有调用它的情况下经过100毫秒时才执行此函数。”

In this article,大卫·沃尔什(David Walsh)解释了此非常流行的去抖动功能,该功能来自underscore.js

function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};

Walsh用来演示如何使用上述功能的示例是:

var myEfficientFn = debounce(function() {
    // All the taxing stuff you do
}, 250);

window.addEventListener('resize', myEfficientFn);

参考文献:

https://davidwalsh.name/javascript-debounce-function

https://css-tricks.com/the-difference-between-throttling-and-debouncing/

https://underscorejs.org/#debounce