首页 > 解决方案 > 状态变量钩子不会在闭包内递增

问题描述

codeandbox.io/s/github/Tmcerlean/battleship

我正在开发一个简单的棋盘游戏,当玩家点击一个有效移动的单元格时,需要增加一个状态变量。

验证移动和进行移动的功能已经到位,但是,我在更新事件侦听器中的状态时遇到了困难。

我可以看到从钩子观察时状态正在更新useEffect,但从函数内部观察时(即使在连续调用之后)也没有。

我做了一些阅读,并相信这可能与过时的关闭有关,但我不确定。


我解决此问题的方法是在用户每次单击后删除然后重新添加单击事件侦听器。

我的假设是这会导致正确的(新增加的)状态变量被拾取。不幸的是,情况似乎并非如此,并且在事件侦听器函数中,该变量永远不会从 0 递增。


我在这里初始化状态变量:

const [placedShips, setPlacedShips] = useState(0);

接下来,将点击事件侦听器应用于游戏板中的每个单元格:

const clickListener = (e) => {
  e.stopImmediatePropagation();
  let direction = currentShip().direction;
  let start = parseInt(e.target.id);
  let end = start + currentShip().length - 1;
  if (playerGameboard.checkValidCoordinates(direction, start, end)) {
    playerGameboard.placeShip(placedShips, direction, start, end);
    setPlacedShips((oldValue) => oldValue + 1);
    console.log(placedShips);
  }
};

const setEventListeners = () => {
  const gameboardArray = Array.from(document.querySelectorAll(".cell"));
  gameboardArray.forEach((cell) => {
    cell.addEventListener("click", (e) => {
      clickListener(e);
    });
  });
};

你会看到setPlacedshipsstate 变量在这里增加了,并且有一个控制台日志来报告它的值。

我知道useState钩子是异步的,因此console.log第一次调用它时会显示 0。因此,我useEffect在函数外部部署了一个钩子,其中还包含一个console.log用于报告更改的值setPlacedShips

useEffect(() => {
  removeEventListeners();
  setEventListeners();
  console.log(placedShips)
}, [placedShips])

每次单击后,placedShips变量都会增加 1,然后运行两个函数:

const removeEventListeners = () => {
  const gameboardArray = Array.from(document.querySelectorAll(".cell"));
  gameboardArray.forEach((cell) => {
    cell.removeEventListener("click", (e) => {
      clickListener(e);
    });
  });
};

紧随其后的是原始setEventListeners函数:

const setEventListeners = () => {
  const gameboardArray = Array.from(document.querySelectorAll(".cell"));
  gameboardArray.forEach((cell) => {
    cell.addEventListener("click", (e) => {
      clickListener(e);
    });
  });
};

如上所述,问题在于setEventListeners函数内的控制台日志始终保持在 0,而useEffect挂钩内的控制台日志按预期递增。

作为参考,这是我目前正在处理的完整组件:

import React, { useEffect, useState, useLayoutEffect } from "react";
import gameboardFactory from "../../factories/gameboardFactory";
import Table from "../Reusable/Table";
import "./GameboardSetup.css";

// -----------------------------------------------
//
// Desc: Gameboard setup phase of game
//
// -----------------------------------------------

let playerGameboard = gameboardFactory();

const GameboardSetup = () => {
  const [humanSetupGrid, setHumanSetupGrid] = useState([]);
  const [ships, _setShips] = useState([
    {
      name: "carrier",
      length: 5,
      direction: "horizontal",
    },
    {
      name: "battleship",
      length: 4,
      direction: "horizontal",
    },
    {
      name: "cruiser",
      length: 3,
      direction: "horizontal",
    },
    {
      name: "submarine",
      length: 3,
      direction: "horizontal",
    },
    {
      name: "destroyer",
      length: 2,
      direction: "horizontal",
    },
  ]);
  const [placedShips, setPlacedShips] = useState(0);

  const createGrid = () => {
    const cells = [];
    for (let i = 0; i < 100; i++) {
      cells.push(0);
    }
  };

  const createUiGrid = () => {
    const cells = [];
    for (let i = 0; i < 100; i++) {
      cells.push(i);
    }
    let counter = -1;
    const result = cells.map((cell) => {
      counter++;
      return <div className="cell" id={counter} />;
    });
    setHumanSetupGrid(result);
  };

  const setUpPlayerGrid = () => {
    // createGrid('grid');
    createUiGrid();
  };

  const currentShip = () => {
    return ships[placedShips];
  };

  const clickListener = (e) => {
    e.stopImmediatePropagation();
    let direction = currentShip().direction;
    let start = parseInt(e.target.id);
    let end = start + currentShip().length - 1;
    if (playerGameboard.checkValidCoordinates(direction, start, end)) {
      playerGameboard.placeShip(placedShips, direction, start, end);
      setPlacedShips((oldValue) => oldValue + 1);
      console.log(placedShips);
    }
  };

  const setEventListeners = () => {
    const gameboardArray = Array.from(document.querySelectorAll(".cell"));
    gameboardArray.forEach((cell) => {
      cell.addEventListener("click", (e) => {
        clickListener(e);
      });
      cell.addEventListener("mouseover", (e) => {
        e.stopImmediatePropagation();
        let direction = currentShip().direction;
        let start = parseInt(cell.id);
        let end = start + currentShip().length - 1;
        if (currentShip().direction === "horizontal") {
          const newShip = [];
          if (playerGameboard.checkValidCoordinates(direction, start, end)) {
            for (let i = start; i <= end; i++) {
              newShip.push(i);
            }
            newShip.forEach((cell) => {
              gameboardArray[cell].classList.add("test");
            });
          }
        } else {
          const newShip = [];
          if (playerGameboard.checkValidCoordinates(direction, start, end)) {
            for (let i = start; i <= end; i += 10) {
              newShip.push(i);
            }
            newShip.forEach((cell) => {
              gameboardArray[cell].classList.add("test");
            });
          }
        }
      });
      cell.addEventListener("mouseleave", (e) => {
        e.stopImmediatePropagation();
        let direction = currentShip().direction;
        let start = parseInt(cell.id);
        let end = start + currentShip().length - 1;
        if (currentShip().direction === "horizontal") {
          const newShip = [];
          if (playerGameboard.checkValidCoordinates(direction, start, end)) {
            for (let i = start; i <= end; i++) {
              newShip.push(i);
            }
            newShip.forEach((cell) => {
              gameboardArray[cell].classList.remove("test");
            });
          }
        } else {
          const newShip = [];
          if (playerGameboard.checkValidCoordinates(direction, start, end)) {
            for (let i = start; i <= end; i += 10) {
              newShip.push(i);
            }
            newShip.forEach((cell) => {
              gameboardArray[cell].classList.remove("test");
            });
          }
        }
      });
    });
  };

  const removeEventListeners = () => {
    const gameboardArray = Array.from(document.querySelectorAll(".cell"));
    gameboardArray.forEach((cell) => {
      cell.removeEventListener("click", (e) => {
        clickListener(e);
      });
    });
  };

  useEffect(() => {
    setUpPlayerGrid();
    // setUpComputerGrid();
  }, []);

  useEffect(() => {
    console.log(humanSetupGrid);
  }, [humanSetupGrid]);

  // Re-render the component to enable event listeners to be added to generated grid
  useLayoutEffect(() => {
    setEventListeners();
  });

  useEffect(() => {
    removeEventListeners();
    setEventListeners();
    console.log(placedShips);
  }, [placedShips]);

  return (
    <div className="setup-container">
      <div className="setup-information">
        <p className="setup-information__p">Add your ships!</p>
        <button
          className="setup-information__btn"
          onClick={() => console.log(placedShips)}
        >
          Rotate
        </button>
      </div>
      <div className="setup-grid">
        <Table grid={humanSetupGrid} />
      </div>
    </div>
  );
};

export default GameboardSetup;

我很困惑这里发生了什么,并且已经在这个问题上停留了几天 - 如果有人有任何建议,那么他们将不胜感激!

谢谢你。

标签: reactjs

解决方案


const removeEventListeners = () => {
  const gameboardArray = Array.from(document.querySelectorAll(".cell"));
  gameboardArray.forEach((cell) => {
    cell.removeEventListener("click", (e) => {
      clickListener(e);
    });
  });
};

上面的代码没有删除任何事件监听器,这可能0是仍然被记录的原因。您将一个新的匿名函数传递给removeEventListener. 由于该函数刚刚创建,它永远不会删除任何事件侦听器,因为它没有注册为事件侦听器。

做同样事情的两个不同的函数是不相等的,这就是为什么不移除事件监听器的原因。

const a = (e) => clickListener(e); // passed to addEventListener
const b = (e) => clickListener(e); // passed to removeEventListener
console.log(a == b); //=> false

要添加和删除事件,您不能使用匿名函数。您要么必须使用命名函数,要么将函数存储在变量中。然后使用函数名或变量注册和移除事件监听器。

由于您只转发event给您,clickListener您可以简单地将您的事件处理程序注册替换为:

cell.addEventListener("click", clickListener);

然后使用以下方法将其删除:

cell.removeEventListener("click", clickListener);

请注意,如果您使用更多 React 方法传递事件处理程序,则可以避免这种情况。cell.addEventHandler(...)您可以在创建此元素时传递事件,而不是使用。例如。<div className='cell' id={counter} onClick={clickListener} />


推荐阅读