状态变量钩子在闭包内不递增

  • 本文关键字:闭包 变量 状态 reactjs
  • 更新时间 :
  • 英文 :


codesandbox.io/s/github/Tmcerlean/战列舰

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

验证移动和进行移动的功能都已就绪,但是,我很难在事件侦听器中更新状态。

我可以看到,当从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);
});
});
};

您将看到setPlacedships状态变量在此处递增,并且有一个控制台日志来报告其值。

我知道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);
});
});
};

紧接着是原始的CCD_ 9函数:

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;

我很困惑这里发生了什么,已经在这个问题上坚持了几天了——如果有人有任何建议,我们将不胜感激!

谢谢。

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

使用React时,应尽量不要手动操作DOM。React Components有Synthetic Events,这意味着您不需要以普通的方式添加事件侦听器。

只需将每个合成事件添加到具有相应处理程序的单元组件中。

您可以在createUiGrid函数中执行此操作:

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} onClick={onClickHandler} onMouseOut={onMouseOutHandler} onMouseOver={onMouseOverHandler} />;
});
setHumanSetupGrid(result);
};

然后只需将您在vanilla上所做的代码移动到每个相应的处理程序(确保在测试之前删除所有侦听器操作)。

最新更新