reactjs - 状态变量钩子不会在闭包内递增
问题描述
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);
});
});
};
你会看到setPlacedships
state 变量在这里增加了,并且有一个控制台日志来报告它的值。
我知道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;
我很困惑这里发生了什么,并且已经在这个问题上停留了几天 - 如果有人有任何建议,那么他们将不胜感激!
谢谢你。
解决方案
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} />
推荐阅读
- amazon-web-services - AWS Athena 表创建失败,“在输入‘创建外部’时没有可行的替代方案”
- image - 如何在 YOLO v3 输出显示视频中捕获框边界图像
- javascript - 网站代码在 Chrome 和 Firefox、桌面或移动设备上运行良好,但在 Safari 上无法运行
- asp.net-mvc - asp.net MVC Webgrid 操作列不起作用
- mariadb - 使用 azure cli 将 maria db 迁移到 azure sql
- api - 无法转换货币 paypal pay API 错误
- clojure - lein test 无法使用 clojure hystrix 正确返回退出代码
- html - 如何在Angular 6中选择特定字段
- apache-spark - Hive - 创建表 LIKE 在 spark-sql 中不起作用
- android - Jenkins Google Play Android Publisher 插件 androidApkMove 不在轨道之间移动发行说明