我正在创建一个小的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);
}
答案 0 :(得分:2)
您可以通过单击数字/跨度标签来重现该问题。
span标记没有子代,因此引发错误。
您可以在这里从我们心爱的CodePen创建者那里了解有关绑定事件如何向其子对象滴流的更多信息: https://css-tricks.com/slightly-careful-sub-elements-clickable-things/
TLDR ;要么检查您正在使用的元素是期望的元素,要么添加一些CSS以防止向下传播(总的来说CSS正在控制此IMHO ...)
✨⭐或只需将代码中的e.target
替换为e.currentTarget
,以便使用该绑定元素,而不要包含其中包含的任何内容。
如果按钮内的任何内容也不需要单击处理,则可以执行此操作。
.btn > * {
pointer-events: none;
}
CSS技巧文章中的评论者提到使用currentTarget
。
我尝试过,它似乎可以工作
只需在代码中将e.target
替换为e.currentTarget
。
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中。我想由于渲染,您甚至可能无法单击以找到此错误,特别是因为它仅存在于新按钮上,但是以编程方式存在于代码中的时间很短。
在使用appendChild
将click事件添加到DOM之前,应将其绑定。在样式设置和事件处理之前,我会避免将元素添加到DOM。
红色区域表示元素在DOM中没有点击处理程序的时间。
如果变量仅分配一次并且不希望重新分配,我建议您使用const
而不是let
。尽管这听起来可能没有用,但可以帮助您了解所定义变量的意图。
我见过很多时候人们遇到了范围和代码方面的问题,只需仔细研究并设置明确的let
与const
有助于阐明问题。
在下面的重构中,我在适用的地方将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;
}
我将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/