【问题标题】:Why does onClick cause a loop?为什么 onClick 会导致循环?
【发布时间】:2020-01-15 21:28:48
【问题描述】:

我正在尝试在 React 中实现扫雷,每当玩家单击地雷时,板都会重置并重新渲染,但玩家最初单击的包含地雷的单元格似乎会在板重置后再次触发 onClick . 我还注意到,如果我在击中地雷后不重置棋盘,而是调用 alert() 然后在不改变状态的情况下返回,那么游戏会循环直到发生堆栈溢出。

当我在游戏结束后显示警报并且不更改状态时,我的有状态棋盘组件的外观如下:

render() {
  let squareGrid = this.state.currentGrid.slice();
  return (
    squareGrid.map((row, y) => { //For each row
        return ( //Create a division
          <div key={y}>
            {
              row.map((state, x) => {//Render a square for each index
                let value = (state.touched) ? state.minedNeighbors :"_";
                return <Square mine={squareGrid[y][x].mine} key={x} disabled={state.touched} val={value}
                  onClick={() => this.handleClick(y, x)}> </Square>

              })}
          </div>
        )
      }
    )
  )
}

handleClick(row, column) {
  // Get copy of grid
  const grid = this.state.currentGrid.slice();
  //If the player clicks a mine, game over.
  if (grid[row][column].mine) {
    //this.resetGame(); //This function does cause a state change

    alert("You have died.");
    return;
  }

  //Non-pure function that mutates grid
  this.revealNeighbors(row, column, grid);

  this.setState({
    currentGrid: grid
  })
}

我的 Square 组件是一个函数

function Square(props) {
    return (
        <button className={"gameButton"} disabled={props.disabled} onClick={props.onClick}>
            {props.val}
        </button>
    );
}

一旦玩家点击了地雷,代码就会一遍又一遍地反复显示警报。 如果我在 handleClick 中取消注释重置游戏的行,板将正确重置,但玩家最后点击的单元格将显示为玩家在板重置后再次点击它。

许多其他帖子遇到我的问题是由于 onClick 属性包含函数调用而不是函数指针,但据我所知,我不是直接在渲染中调用函数;我提供了一个闭包。

编辑: 这是我的 Board 组件的完整代码。

class Board extends React.Component {
    constructor(props) {
        super(props);
        let grid = this.createGrid(_size);

        this.state = {
            size: _size,
            currentGrid: grid,
            reset: false
        }
    }

    createGrid(size) {
        const grid = Array(size).fill(null);
        //Fill grid with cell objects
        for (let row = 0; row < size; row++) {
            grid[row] = Array(size).fill(null);
            for (let column = 0; column < size; column++) {
                grid[row][column] = {touched: false, mine: Math.random() < 0.2}
            }
        }

        //Reiterate to determine how many mineNeighbors each cell has
        for (let r = 0; r < size; r++) {
            for (let c = 0; c < size; c++) {
                grid[r][c].minedNeighbors = this.countMineNeighbors(r, c, grid)
            }
        }

        return grid;
    }

    handleClick(row, column) {
        const grid = this.state.currentGrid.slice();

        //If the player clicks a mine, game over.
        if (grid[row][column].mine) {
            //this.resetGame();
            //grid[row][column].touched = true;
            alert("You have died.");
            return;
        }

        //Non-pure function that mutates grid
        this.revealNeighbors(row, column, grid);

        this.setState({
            currentGrid: grid
        })
    }

    //Ensure cell is in bounds
    checkBoundary(row, column) {
        return ([row, column].every(x => 0 <= x && x < this.state.size));
    }


    revealNeighbors(row, column, grid) {
        //Return if out of bounds or already touched
        if (!this.checkBoundary(row, column) || grid[row][column].touched) {
            return;
        }

        //Touch cell
        grid[row][column].touched = true;

        if (grid[row][column].minedNeighbors === 0) {
            //For each possible neighbor, recurse.
            [[1, 0], [-1, 0], [0, 1], [0, -1]]
                .forEach(pos => this.revealNeighbors(row + pos[0], column + pos[1], grid));
        }

    }


    countMineNeighbors(row, column, grid) {
        let size = grid.length;

        //Returns a coordinate pair representing the position of the cell in the direction of the angle, eg, Pi/4 radians -> [1,1]
        let angleToCell = (angle) => [Math.sin, Math.cos]
            .map(func => Math.round(func(angle)))
            .map((val, ind) => val + [row, column][ind]);

        return Array(8)
            .fill(0)
            .map((_, ind) => ind * Math.PI / 4) //Populate array with angles toward each neighbor
            .map(angleToCell)
            .filter(pos => pos.every(x => 0 <= x && x < size))//Remove out of bounds cells
            .filter(pos => grid[pos[0]][pos[1]].mine)//Remove cells that aren't mines
            .length //Return the length of the array as the count
    }

    resetGame() {
        this.setState({
                currentGrid: this.createGrid(this.state.size)
            }
        )
    }

    render() {
        let squareGrid = this.state.currentGrid.slice();
        return (
            squareGrid.map((row, y) => { //For each rows
                    return ( //Create a division
                        <div key={y}>
                            {
                                row.map((state, x) => {//Render a square for each index
                                    let value = (state.touched) ? state.minedNeighbors : "_";
                                    return <Square mine={squareGrid[y][x].mine} key={x} disabled={state.touched} val={value}
                                                   onClick={() => this.handleClick(y, x)}/>

                                })}
                        </div>
                    )
                }
            )
        )
    }
}

【问题讨论】:

  • 控制台输出中是否有任何警告或错误?
  • 这个this.revealNeighbors发生了什么?
  • 您能否提供一个交互式示例(即 jsfiddle)或至少提供完整的组件代码?
  • 除了无限循环之外,我没有看到任何错误。
  • @BrianThompson 递归搜索提供给它的本地网格,并触摸所有没有我的邻居的单元格。它不会更改全局状态,而是返回修改后的本地网格。我已经提供了它的代码。

标签: javascript reactjs


【解决方案1】:

当你改变你的状态时,你的渲染方法就会被调用。

this.setState({
        currentGrid: grid
    })

您可能应该实现一个名为 shouldComponentUpdate 的方法来防止这种情况发生。此外,您的切片没有得到解决。我建议您尝试使用 async/await。

【讨论】:

  • 我明白了,但是因为this.resetGame()被注释掉了,所以玩家点击一个地雷时状态没有改变,而且因为函数之后返回,我不明白为什么会重新-render 或者为什么会再次调用 onClick。
  • 因为你的代码 const grid = this.state.currentGrid.slice();没有及时解决。所以 async handleClick(... = await this.state.currentGrid.slice(); 看看会发生什么。
  • 我试过了,但得到了相同的结果;无限警报。
【解决方案2】:

如果您单击一个重新渲染的元素,我会遇到这样的问题。我不确定这是否会在您的特定情况下解决问题,但我找到了 2 个过去对我有用的解决方案。

一种是在你的鼠标点击事件中放置一个标志,

if(!mouseDownFlag){
mouseDownFlag = true;
//the rest of your onetime code
}

然后在 mouseupevent 上移除标志

或者,有时使用 mousedown 事件而不是 mouseclick,可以更可预测。

希望这些解决方案之一有所帮助。

【讨论】:

    【解决方案3】:

    你需要改变你传递和使用点击函数的方式(当传递一个函数作为道具时你只想传递对函数的引用而不是调用它,因此不包括()

     return 
         <Square 
             mine={squareGrid[y][x].mine} 
             key={x} disabled={state.touched} 
             val={value}
             // **** change line below
             onClick={this.handleClick}
         > </Square>
    

    当你调用它时

     <button 
         className={"gameButton"} 
         disabled={props.disabled} 
         // **** change line below
         onClick={() => props.onClick()}>
             {props.val}
    </button>
    

    【讨论】:

    • 看起来他们是在声明一个内联函数,而不是执行它。道具看起来正确。
    • 在我的渲染函数中,我没有调用该函数,而是传入了一个包含该函数的箭头函数。如果我没记错的话,它已经看起来像你写的那样了。另外,如果我在不使用闭包的情况下调用它,我会丢失参数。
    • @Tayler Cooper,据我所知,您的情况与应有的相反,您用括号传递它并在没有括号的情况下调用它。在我的回答中,我只是将它们交换为正确的顺序(在&lt;Square&gt; 中您正在传递它,在&lt;button&gt; 中您正在调用该函数),这就是我所理解的,尽管我可能是错的。
    猜你喜欢
    • 2013-04-30
    • 2013-06-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-06
    相关资源
    最近更新 更多