一个Heisenbug:在JavaScript中组合不同的输入类型时出现意外行为。

huangapple go评论76阅读模式
英文:

A Heisenbug: Unexpected behavior when combining different input types in JavaScript

问题

我目前正在进行我的计算器学校项目。它可以正常工作,如果我分别使用按钮或键盘输入,但是在混合输入的情况下出现了问题,特别是在使用"Enter"按钮时。这是我的完整项目链接:codepen

描述问题的最佳方法是提供一个简要的示例,比如"1 + 2"。

可能的选项:

  1. 仅使用按钮,结果是正确的。
  2. 仅使用键盘,结果是正确的。
  3. 使用混合输入和"="键,结果是正确的。
  4. 使用混合输入和"Enter"键,发生了意外行为。

意外行为是什么?

  1. 如果第一个数字是通过按钮输入的,但第二个数字是通过键盘输入的,那么显示的是第一个数字而不是结果。
  2. 如果第二个数字或两个数字都是通过按钮输入的,那么显示的是第二个数字而不是结果。

在这两种情况下,连续按下"Enter"键会将显示的值乘以,而不是出现错误。

  1. 两个数字都是通过键盘输入的,但操作符是通过按钮输入的:程序会给出正确的结果,但会在后面添加操作符并冻结。

这是我如何处理按钮点击的方式:

function handleButtonClick(event) {
    const buttonClicked = event.target;

    hideErrorIfEqualErrorDisplayed('mouse', buttonClicked);

    switch (buttonClicked) {
        case buttonDecimalPoint:
            handleDecimalPointButton();
            break;
        case buttonClear:
            clearAll();
            break;
        case buttonDelete:
            deleteLastSymbol();
            break;
        case buttonEqual:
            handleEqualSign();
            break;
        default:
            const classButton = buttonClicked.classList;
            const valueOfClickedButton = buttonClicked.textContent;

            if (classButton.contains('button-digit')) {
                handleDigitButton(valueOfClickedButton);
            } else if (classButton.contains('button-operator')) {
                handleOperatorButton(valueOfClickedButton);
            }
            break;
    }
}

这是我如何处理键盘输入的方式:

function handleKeyDown(event) {
    const keyPressed = event.key;

    hideErrorIfEqualErrorDisplayed('keyboard', keyPressed);

    switch (keyPressed) {
        case SYMBOLS.DOT:
          if (isDecimalPointDisabled) {
            event.preventDefault();
          } else {
            handleDecimalPointButton();
          }
          break;
    
        case SYMBOLS.BACKSPACE:
          deleteLastSymbol();
          break;
    
        case SYMBOLS.ESCAPE:
          clearAll();
          break;
    
        case SYMBOLS.EQUAL:
        case SYMBOLS.ENTER:
            console.log(keyPressed);
          if (isZeroError) {
            event.preventDefault();
          } else {
            console.log(`current display: ${containerDisplay.textContent}`);
            console.log('Go to handleEqualSign()');
            handleEqualSign();
          }
          console.log('break');
          break;
    
        default:
          if (checkIfDigit(keyPressed)) {
            handleDigitButton(keyPressed);
          } else if (Object.values(OPERATORS).includes(keyPressed)) {
            if (areOperatorsDisabled) {
              event.preventDefault();
            } else {
              handleOperatorButton(keyPressed);
            }
          } else {
            event.preventDefault();
          }
          break;
      }
}

以下是其他函数:

function handleEqualSign(){
    if (!isSecondNumber) {
        showErrorMessage(errorEqual);
        return;
    }

    console.log('Go to getResult()');
    getResult();
}

function hideErrorIfEqualErrorDisplayed(inputSource, symbolTyped) {
    if (isEqualError) {
        if (inputSource === 'mouse' && symbolTyped !== buttonEqual) {
            hideErrorMessage(errorEqual);
        } else if (inputSource === 'keyboard' && symbolTyped !== SYMBOLS.ENTER && symbolTyped !== SYMBOLS.EQUAL) {
            hideErrorMessage(errorEqual);
        }
    }
}

function handleOperatorButton(operatorValue) {
    disableButton(buttonDecimalPoint);

    if (isOperator) {
        deleteLastSymbol();
    }

    if (!isSecondNumber) {
        shiftFromFirstToOperator();   
    } else {
        getResult(isAfterEqualClick=false);
    }

    addToDisplay(operatorValue, gap=true)

    operator = operatorValue;  
}

function getResult(isAfterEqualClick=true) {
    const result = operate(parseFloat(number1), parseFloat(number2), operator);
    console.log(`getResult() arguments: ${parseFloat(number1)}, ${parseFloat(number2)}, ${operator} `);
    console.log(`Result: ${result}`);

    containerDisplay.textContent = parseFloat(result.toFixed(5));
    console.log(`containerDisplay.textContent: ${containerDisplay.textContent }`);
    
    number1 = result.toString();
    isResult = true;
    number2 = EMPTY; 
    isSecondNumber = false;

    if (isAfterEqualClick) {
        console.log('isAfterEqualClick = True');
        isFirstNumber = true;
        isOperator = false;
        operator = EMPTY;
    } else {
        console.log('isAfterEqualClick = False');
        shiftFromFirstToOperator();
    }

    if (!Number.isInteger(result)) {
        disableButton(buttonDecimalPoint);
    } else {
        enableDecimalPointIfDisabled();
    } 
}

function handleDigitButton(digitValue) { 
    if (isFirstNumber) { 
        handleFirstNumber(digitValue);
    } else if (isOperator && !isSecondNumber) { 
        handleStartOfSecondNumber(digitValue);
    } else {  
        handleContinueOfSecondNumber(digitValue);
    }
} 

function add(addend1, addend2) {
    return addend1 + addend2;
}

我已经完成的工作:

  1. 问题是这个错误在检查模式下不会发生,它在那里正常工作。
  2. 日志记录提供了预期的值,因此没有帮助。
  3. 问题在不同的浏览器中都一样,在调试模式下它消失了。
英文:

I am currently working on my calculator school project.
It works fine if I use either a button or a keyboard input separately, however, something goes wrong in case of the mixed input, particularly when the Enter button is used.
Here is my full project: codepen.
The best way to describe problem is to provide a brief example, just 1 + 2.
Possible options:

  1. Use only buttons, the result is correct.
  2. Use only keys, the result is correct.
  3. Use mixed input and the = key, the result is correct.
  4. Use mixed input and the Enter key, the unexpected behaviour happens.

What is an unexpected behaviour?

  1. If the first number was entered by a button, but the second one by the keyboard, the first number is displayed instead of result.
  2. If the second or both numbers were introduced by buttons, the second number is displayed instead of result.

After both this cases, a consequent Enter press just multiply the displayed value instead of giving the error.

  1. Both numbers were entered by a keyboard, but an operator by a button: the program gives a right result but adds the operator after and freeze.

Here is how I handle a button click:

function handleButtonClick(event) {
const buttonClicked = event.target;
hideErrorIfEqualErrorDisplayed('mouse', buttonClicked);
switch (buttonClicked) {
case buttonDecimalPoint:
handleDecimalPointButton();
break;
case buttonClear:
clearAll();
break;
case buttonDelete:
deleteLastSymbol();
break;
case buttonEqual:
handleEqualSign();
break;
default:
const classButton = buttonClicked.classList;
const valueOfClickedButton = buttonClicked.textContent;
if (classButton.contains('button-digit')) {
handleDigitButton(valueOfClickedButton);
} else if (classButton.contains('button-operator')) {
handleOperatorButton(valueOfClickedButton);
}
break;
}
}

That's how I handle a key input:

function handleKeyDown(event) {
const keyPressed = event.key;
hideErrorIfEqualErrorDisplayed('keyboard', keyPressed);
switch (keyPressed) {
case SYMBOLS.DOT:
if (isDecimalPointDisabled) {
event.preventDefault();
} else {
handleDecimalPointButton();
}
break;
case SYMBOLS.BACKSPACE:
deleteLastSymbol();
break;
case SYMBOLS.ESCAPE:
clearAll();
break;
case SYMBOLS.EQUAL:
case SYMBOLS.ENTER:
console.log(keyPressed);
if (isZeroError) {
event.preventDefault();
} else {
console.log(`current display: ${containerDisplay.textContent}`);
console.log('Go to handleEqualSign()');
handleEqualSign();
}
console.log('break');
break;
default:
if (checkIfDigit(keyPressed)) {
handleDigitButton(keyPressed);
} else if (Object.values(OPERATORS).includes(keyPressed)) {
if (areOperatorsDisabled) {
event.preventDefault();
} else {
handleOperatorButton(keyPressed);
}
} else {
event.preventDefault();
}
break;
}
}

Here are other functions:

function handleEqualSign(){
if (!isSecondNumber) {
showErrorMessage(errorEqual);
return;
}
console.log('Go to getResult()');
getResult();
}
function hideErrorIfEqualErrorDisplayed(inputSource, symbolTyped) { // inputSource: keyboard or mouse
if (isEqualError) {
if (inputSource === 'mouse' && symbolTyped !== buttonEqual) {
hideErrorMessage(errorEqual);
} else if (inputSource === 'keyboard' && symbolTyped !== SYMBOLS.ENTER && symbolTyped !== SYMBOLS.EQUAL) {
hideErrorMessage(errorEqual);
}
}
}
function handleOperatorButton(operatorValue) {
disableButton(buttonDecimalPoint);
if (isOperator) { // Display and use the last operator if the user has clicked more than one in a row
deleteLastSymbol();
}
if (!isSecondNumber) {
shiftFromFirstToOperator();   
} else {
getResult(isAfterEqualClick=false);
}
addToDisplay(operatorValue, gap=true)
operator = operatorValue;  
}
function getResult(isAfterEqualClick=true) {  // isAfterEqualClick = true when we enter the function after the clicking '=' or pressing the 'Enter' key, false after clicking any other operator.
const result = operate(parseFloat(number1), parseFloat(number2), operator);
console.log(`getResult() arguments: ${parseFloat(number1)}, ${parseFloat(number2)}, ${operator} `);
console.log(`Result: ${result}`);
containerDisplay.textContent = parseFloat(result.toFixed(5));
console.log(`containerDisplay.textContent: ${containerDisplay.textContent }`);
number1 = result.toString();
isResult = true;
number2 = EMPTY; 
isSecondNumber = false;
if (isAfterEqualClick) {
console.log('isAfterEqualClick = True');
isFirstNumber = true;
isOperator = false;
operator = EMPTY;
} else {
console.log('isAfterEqualClick = False');
shiftFromFirstToOperator();
}
if (!Number.isInteger(result)) {
disableButton(buttonDecimalPoint);
} else {
enableDecimalPointIfDisabled();
} 
}
function handleDigitButton(digitValue) { 
if (isFirstNumber) { 
handleFirstNumber(digitValue);
} else if (isOperator && !isSecondNumber) { 
handleStartOfSecondNumber(digitValue);
} else {  
handleContinueOfSecondNumber(digitValue);
}
} 
function add(addend1, addend2) {
return addend1 + addend2;
}

What I have already done:

  1. The issue is that this bug doesn't occur in the inspect mode, it works properly there.
  2. Logging provides expected values so doesn't help.
  3. The problem is the same in different browser, in the debug mode it disappears.

答案1

得分: 1

我刚刚完成了调试您的代码。

因此,我认为您的Heisenbug是由enter键事件引起的。当您单击按钮时,按钮会获得焦点。然后,如果您按下enter键,handleKeyDown()handleButtonClick()都会连续执行。结果,您的display div首先显示正确的结果,然后再次按下按下的按钮,导致错误。

要修复此错误,您可以使用blur()方法从按钮上移除焦点。

例如:

function handleButtonClick(event) {
  const buttonClicked = event.target;
  buttonClicked.blur();
  /* ... */
}

<h1>已更新的答案</h1>

或者,您可以使用preventDefault()方法阻止它触发按钮上的点击事件。

例如:

case SYMBOLS.EQUAL:
case SYMBOLS.ENTER:
  if (!isZeroError) {
    handleEqualSign();
  }
  event.preventDefault();
英文:

I have just finished debugging your code.

As a result, I think your heisenbug is caused by the enter key event. When you click the button, the button gets focus. Then, if you press the enter key, both handleKeyDown() and handleButtonClick() are executed continuously. As a result, your display div shows the correct result first and then the pressed button is re-pressed, causing errors.

To fix this error, you can use the blur() method to remove the focus from the button.

For example:

function handleButtonClick(event) {
const buttonClicked = event.target;
buttonClicked.blur();
/* ... */
}

<h1>UPDATED ANSWER</h1>

Alternatively, you can use the preventDefault() method to stop it from triggering a click event on the button.

For example:

case SYMBOLS.EQUAL:
case SYMBOLS.ENTER:
if (!isZeroError) {
handleEqualSign();
}
event.preventDefault();

答案2

得分: 0

只需在handleButtonClick函数中检查event.pointerType不是Mouse,它就会起作用。谢谢!

英文:

Just check for event.pointerType to not be Mouse in the handleButtonClick function, it works. Thanks!

huangapple
  • 本文由 发表于 2023年7月17日 12:21:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76701473.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定